回顾我们前面的话题,继续拿 C/S 架构与 B/S 架构对比。在 C/S 编程中,我们从来没有苦恼过代码重用的问题,因为面向对象给了我们很好的解决方案。但在 B/S 编程中,如何解决页面内容的代码重用问题?在 Apusic OperaMasks中,模版与复合组件登场了(更确切的说法是, Facelets带来了模版与复合组件,但 AOM 能够让它们的威力发挥得淋漓尽致)。
所谓模版,顾名思义:是指定一个大体不会变化的页面作为公用模版,在使用模版的时候,只需替换其中需要更改的内容。
复合组件则恰恰相反,它是将某些可公用的页面内容封装起来,形成一个新的组件,然后再把这个新的组件拿出来复用。
这两种方案,都能够有效解决页面级的代码重用问题,但笔者个人认为:复合组件的使用技巧更复杂,更巧妙,适用面也更广。 本文,笔者会给大家描述一种适合使用复合组件的场景,并介绍在 Apusic OperaMasks 中,一个复合组件是如何一步一步衍化,从而更好的满足我们需求的。
在我们的日常工作中,经常会遇到这样的页面:

针对上述页面,常规作法是,用一个分成两列的布局组件(panelGrid或者table),第一列放 label,第二列放需要的组件。但是,大量类似的页面,将导致我们的工作非常繁琐,倘若能够有一个自身就带 label 的“智能组件”,自然可以简化许多工作。
下面,就让我们动手开发一个这样的复合组件,不妨称之为“SmartWidget”。
本示例所用到的环境分别是:
Apusic OperaMasks 2.1:http://www.operamasks.org/dist/aom/v2.1/
Apusic 应用服务器 5.1:http://www.apusic.com/dist/apusicAS/v5.1/
Apusic Studio 5.1M5 Path 4:http://www.apusic.com/dist/apusicStudio/v5.1-M5/
首先,需要在Web应用中更改一下 web.xml,加上如下参数:
<context-param>
<param-name>facelets.LIBRARIES</param-name>
<param-value>/WEB-INF/tags/composition_component.taglib.xml</param-value>
</context-param>
此处
是指,跟复合组件相关的定义文件位于 /WEB-INF/tags/composition_component.taglib.xml 内。
再来看一下 composition_component.taglib.xml 包含哪些内容:
<!DOCTYPE facelet-taglib PUBLIC
"-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
"http://java.sun.com/dtd/facelet-taglib_1_0.dtd">
<facelet-taglib>
<namespace>http://www.operamasks.org/demo/composition</namespace>
<tag>
<tag-name>smartWidget1</tag-name>
<source>smartWidget1.xhtml</source>
</tag>
</facelet-taglib>
至此,一个复合组件所需要的基本框架已经搭建完毕。
现在,我们先完成最简单的情况:只生成一个 label 和一个 textField,复合组件定义文件的代码示例如下:
smartWidget1.xhtml
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets">
<w:page title="smart widget">
<!--
使用说明:
<my:smartWidget1 label="name" value="#{myBean.name}"/>
其中:
1) label:必须项,指定显示文本
2) value:必须项,指定 textField 的绑定值
-->
<ui:composition>
<table border="0" cellpadding="0" cellspacing="0" style="margin: 0px; padding: 0px;">
<tr>
<td>
<h:outputText value="#{label}" />
</td>
<td>
<w:textField value="#{value}"/>
</td>
</tr>
</table>
</ui:composition>
</w:page>
</f:view>
可见,开发一个复合组件其实是非常简单的,下面我们来做一个测试案例,以观察运行效果。
testSmartWidget1.xhtml
<!DOCTYPE HTML PUBLIC "" "">
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:my="http://www.operamasks.org/demo/composition" renderKitId="AJAX">
<w:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</w:head>
<w:page title="test smart widget1">
<div align="center" style="font-size: 11pt; padding-top: 20px;">
<w:form>
<layout:panelGrid columns="1">
<layout:cell>
<my:smartWidget1 label="姓名:" value="#{testSmartWidget1Bean.name}" />
</layout:cell>
<layout:cell>
<w:button id="ok" />
</layout:cell>
</layout:panelGrid>
</w:form>
</div>
</w:page>
</f:view>
![]() |
此处声明复合组件的命名空间,注意,此命名空间在 composite_component.tablig.xml 文件( |
![]() |
使用smartWidget复合组件,指定其 label 值和 value 值。 |
此页面所使用到的 ManagedBean 如下:
TestSmartWidget1Bean:
@ManagedBean(name="testSmartWidget1Bean", scope=ManagedBeanScope.REQUEST)
public class TestSmartWidget1Bean implements Serializable {
@ManagedProperty
private String name;
@Action(id="ok")
public void ok(){
System.out.println("name=" + name);
}
}
页面运行效果如下:

一个复合组件便开发完毕了,非常之简单。
但我们很快就发现 SmartWidget 的不足:你无法指定 label 和 textField 的宽度,这无疑是不可接受的。我们希望做到的效果是,如下使用复合组件:
<my:smartWidget2 label="姓名:" value="#{testSmartWidget2Bean.name}" width="50, 200"/>
其中,width的值以','分隔,前一部分是 label 的 width,后一部分是 widget 的 width。
于是,对 smartWidget2.xhtml 做出以下衍化:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jstl/core" xmlns:h="http://java.sun.com/jsf/html"
xmlns:om="http://www.apusic.com/jsf/misc">
<w:page title="smart widget">
<!--
使用说明:
<my:smartWidget2 label="name" value="#{myBean.name} width="60,150"/>
其中:
1) label:必须项,指定显示文本
2) value:必须项,指定 textField 的绑定值
3) width,可选项, 其值以','分隔,前一部分是 label 的 width,后一部分是 widget 的 width
width 的缺省值是:60, 150
-->
<ui:composition>
<om:elite>
<![CDATA[
define labWidth = 60;
define widgetWidth = 150;
if(defined(width)) {
define array = (String(width)).split(',');
labWidth = array[0];
widgetWidth = array[1];
}
]]>
</om:elite>
<table border="0" cellpadding="0" cellspacing="0" style="margin: 0px; padding: 0px;">
<tr>
<td width="#{labWidth}">
<h:outputText value="#{label}" />
</td>
<td>
<w:textField value="#{value}"width="#{widgetWidth}"/>
</td>
</tr>
</table>
</ui:composition>
</w:page>
</f:view>
![]() |
此处是一段 ELite 代码。关于 ELite 的详细介绍,本文暂不涉及。此处的代码也非常简单,相信写过程序的同学都能看懂。 |
![]() |
指定 label 的 width |
![]() |
指定 textField 的 width |
测试页面如下:
<!DOCTYPE HTML PUBLIC "" "">
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ajax="http://www.apusic.com/jsf/ajax"
xmlns:my="http://www.operamasks.org/demo/composition" renderKitId="AJAX"
xmlns:ui="http://java.sun.com/jsf/facelets">
<w:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</w:head>
<w:page title="test smart widget2">
<ui:debug hotkey="E"></ui:debug>
<div align="center" style="font-size: 10pt; padding-top: 20px;">
<w:form>
<layout:panelGrid columns="2">
<layout:cell>用法:</layout:cell>
<layout:cell><my:smartWidget2 label="姓名:" value="\#{testSmartWidget2Bean.name}"/></layout:cell>
<layout:cell>效果:</layout:cell>
<layout:cell><my:smartWidget2 label="姓名:" value="#{testSmartWidget2Bean.name}"/></layout:cell>
<layout:cell>用法:</layout:cell>
<layout:cell>
<my:smartWidget2 label="姓名:" value="\#{testSmartWidget2Bean.name}" width="50, 200"/>
</layout:cell>
<layout:cell>效果:</layout:cell>
<layout:cell>
<my:smartWidget2 label="姓名:" value="#{testSmartWidget2Bean.name}" width="50, 200"/>
</layout:cell>
<layout:cell>用法:</layout:cell>
<layout:cell>
<my:smartWidget2 label="姓名:" value="\#{testSmartWidget2Bean.name}" width="100, 300"/>
</layout:cell>
<layout:cell>效果:</layout:cell>
<layout:cell>
<my:smartWidget2 label="姓名:" value="#{testSmartWidget2Bean.name}" width="100,300"/>
</layout:cell>
<layout:cell colspan="2"><w:button id="ok" /></layout:cell>
</layout:panelGrid>
</w:form>
</div>
</w:page>
</f:view>
运行效果如下:

但是,目前的 SmartWidget 依然无法大规模使用,因为我们不可能只用到 textField,我们还需要 dateField、numberField等等其它组件。我们希望,通过指定 type,就可以生成想要的组件。如:
<my:smartWidget3 label="注册日期:" value="#{testSmartWidget3Bean.date}" type="dateField"/>
则自动生成日期控件 dateField。
继续我们的衍化之路,将 smartWidget3.xhtml 修订为:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jstl/core" xmlns:h="http://java.sun.com/jsf/html"
xmlns:om="http://www.apusic.com/jsf/misc">
<w:page title="smart widget">
<!--
使用说明:
<my:smartWidget2 label="name" value="#{myBean.name} width="60,150" type="textField||dateField||numberField"/>
其中:
1) label:必须项,指定显示文本
2) value:必须项,指定 textField 的绑定值
3) width:可选项, 其值以','分隔,前一部分是 label 的 width,后一部分是 widget 的 width
width 的缺省值是:60, 150
4) type: 可选项,其值可以是 textField 或者 dateField 或者 numberField,缺省值是 textField
-->
<ui:composition>
<om:elite>
<![CDATA[
define labWidth = 60;
define widgetWidth = 150;
if(defined(width)) {
define array = (String(width)).split(',');
labWidth = array[0];
widgetWidth = array[1];
}
if(!defined(type)) {
define type = "textField";
}
]]>
</om:elite>
<table border="0" cellpadding="0" cellspacing="0" style="margin: 0px; padding: 0px;">
<tr>
<td width="#{labWidth}">
<h:outputText value="#{label}" />
</td>
<td>
<c:choose>
<c:when test="#{type.equals('dateField')}">
<w:dateField value="#{value}" width="#{widgetWidth}" />
</c:when>
<c:when test="#{'numberField'.equals(type)}">
<w:numberField value="#{value}" width="#{widgetWidth}" />
</c:when>
<c:otherwise>
<w:textField value="#{value}" width="#{widgetWidth}" />
</c:otherwise>
</c:choose>
</td>
</tr>
</table>
</ui:composition>
</w:page>
</f:view>
再来看一下测试页面:
<!DOCTYPE HTML PUBLIC "" "">
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ajax="http://www.apusic.com/jsf/ajax"
xmlns:my="http://www.operamasks.org/demo/composition" renderKitId="AJAX"
xmlns:ui="http://java.sun.com/jsf/facelets">
<w:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</w:head>
<w:page title="test smart widget3">
<div align="center" style="font-size: 10pt; padding-top: 20px;">
<w:form>
<layout:panelGrid columns="1">
<layout:cell>
<my:smartWidget3 label="姓名:" value="#{testSmartWidget3Bean.name}" width="80,150"/>
</layout:cell>
<layout:cell>
<my:smartWidget3 label="年龄:" value="#{testSmartWidget3Bean.age}" width="80,150"type="numberField"/>
</layout:cell>
<layout:cell>
<my:smartWidget3 label="注册日期:" value="#{testSmartWidget3Bean.date}" width="80,150"type="dateField"/>
</layout:cell>
<layout:cell>
<w:button id="ok" />
</layout:cell>
</layout:panelGrid>
</w:form>
</div>
</w:page>
</f:view>
运行效果如下:

看上去好像一切都很美好,但事实上,如果你仔细思考一下,你会发觉如下问题:
1) 为什么不生成下拉列表 combo?这也是频繁使用的组件啊!
2) 如何监控生成的组件事件?譬如:我需要监听某个 textField 的 onchange 事件。
其实造成这两个问题的根本原因都是一个:你无法对这些不同的组件做统一的抽象。举个简单例子:textField 的值可以通过 value 属性指定,而下拉列表框combo的值则无法仅通过 value 属性获得,它还需要指定 selectItems。再譬如,combo 有 onselect 事件,而 textField 没有此事件,针对这些非公用属性,你如何封装(更确切的说法是:不是不能封装,而是如果进行这类差异性封装的话, 复合组件会写的非常辛苦)?
那么,能不能换种思路:我们并不试图去封装所有的组件属性,我们只给出组件的必要属性, 通过此属性,就能够获得其它任意特性的扩展呢?
这个属性就是id,而它所依赖的就是IoVC。再次重申一下:这是一种新的编程思想,请参考:《IoVC,一种新的编程思想》
由此,我们对 SmartWidget做了第四次衍化:
smartWidget4.xhtml:
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jstl/core" xmlns:h="http://java.sun.com/jsf/html"
xmlns:om="http://www.apusic.com/jsf/misc">
<w:page title="smart widget">
<!--
使用说明:
<my:smartWidget2 id="theId" label="name" value="#{myBean.name} width="60,150"
type="textField||dateField||numberField||combo"
selectItemsIt="theItemsId"/>
其中:
1) id,可选项,用作IoVC绑定
2) value,变成可选项,如果通过IoVC绑定,则不需要指定此属性
3) width,可选项,其值以 ',' 分隔,前一部分是 label 的width,后一部分是 widget 的 width,默认值为 60,150
4) type是可选属性,其值可以是 textField 或者 dateField 或者 numberField 或者 combo,缺省值是 textField。
如果 type 指定 combo,只能用 IoVC 绑定,并且要指定 selectItemsId
5) selectItemsId: 如果 type 指定 combo,则必须要指定此属性。否则,此属性则无需指定
-->
<ui:composition>
<om:elite>
<![CDATA[
define labWidth = 60;
define widgetWidth = 150;
if(defined(width)) {
define array = (String(width)).split(',');
labWidth = array[0];
widgetWidth = array[1];
}
if(!defined(type)) {
define type = "textField";
}
]]>
</om:elite>
<table border="0" cellpadding="0" cellspacing="0" style="margin: 0px; padding: 0px;">
<tr>
<td width="#{labWidth}">
<h:outputText value="#{label}" />
</td>
<td>
<c:choose>
<c:when test="#{type.equals('dateField')}">
<c:if test="#{defined(id) && defined(value)}">
<w:dateField id="#{id}" value="#{value}" width="#{widgetWidth}" />
</c:if>
<c:if test="#{defined(id) && !defined(value) }">
<w:dateField id="#{id}" width="#{widgetWidth}" />
</c:if>
<c:if test="#{defined(value) && !defined(id) }">
<w:dateField value="#{value}" width="#{widgetWidth}" />
</c:if>
<c:if test="#{!defined(id) && !defined(value) }">
<w:dateField width="#{widgetWidth}" />
</c:if>
</c:when>
<c:when test="#{'numberField'.equals(type)}">
<c:if test="#{defined(id) && defined(value)}">
<w:numberField id="#{id}" value="#{value}" width="#{widgetWidth}" />
</c:if>
<c:if test="#{defined(id) && !defined(value) }">
<w:numberField id="#{id}" width="#{widgetWidth}" />
</c:if>
<c:if test="#{defined(value) && !defined(id) }">
<w:numberField value="#{value}" width="#{widgetWidth}" />
</c:if>
<c:if test="#{!defined(id) && !defined(value)}">
<w:numberField width="#{widgetWidth}" />
</c:if>
</c:when>
<c:when test="#{'combo'.equals(type)}">
<c:if test="#{defined(id) && defined(value)}">
<w:combo id="#{id}" value="#{value}" width="#{widgetWidth}">
<f:selectItems id="#{selectItemsId}"/>
</w:combo>
</c:if>
<c:if test="#{defined(id) && !defined(value) }">
<w:combo id="#{id}" width="#{widgetWidth}">
<f:selectItems id="#{selectItemsId}"/>
</w:combo>
</c:if>
<c:if test="#{defined(value) && !defined(id) }">
<w:combo value="#{value}" width="#{widgetWidth}" >
<f:selectItems id="#{selectItemsId}"/>
</w:combo>
</c:if>
<c:if test="#{!defined(id) && !defined(value) }">
<w:combo width="#{widgetWidth}">
<f:selectItems id="#{selectItemsId}"/>
</w:combo>
</c:if>
</c:when>
<c:otherwise>
<c:if test="#{defined(id) && defined(value)}">
<w:textField id="#{id}" value="#{value}" width="#{widgetWidth}" />
</c:if>
<c:if test="#{defined(id) && !defined(value) }">
<w:textField id="#{id}" width="#{widgetWidth}" />
</c:if>
<c:if test="#{defined(value) && !defined(id) }">
<w:textField value="#{value}" width="#{widgetWidth}" />
</c:if>
<c:if test="#{!defined(id) && !defined(value) }">
<w:textField width="#{widgetWidth}" />
</c:if>
</c:otherwise>
</c:choose>
</td>
</tr>
</table>
</ui:composition>
</w:page>
</f:view>
测试页面 testSmartWidget4.xhtml:
<!DOCTYPE HTML PUBLIC "" "">
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ajax="http://www.apusic.com/jsf/ajax"
xmlns:my="http://www.operamasks.org/demo/composition" renderKitId="AJAX"
xmlns:ui="http://java.sun.com/jsf/facelets">
<w:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</w:head>
<w:page title="test smart widget4">
<div align="center" style="font-size: 10pt; padding-top: 20px;">
<w:form>
<layout:panelGrid columns="1">
<layout:cell>
<my:smartWidget4 label="姓名:" id="name" width="80,150"/>
</layout:cell>
<layout:cell>
<my:smartWidget4 label="性别:" id="gender" width="80,150" type="combo" selectItemsId="genders"/>
</layout:cell>
<layout:cell>
<my:smartWidget4 label="年龄:" id="age" width="80,150" type="numberField"/>
</layout:cell>
<layout:cell>
<my:smartWidget4 label="注册日期:" id="date" width="80,150" type="dateField"/>
</layout:cell>
<layout:cell>
<w:button id="ok" />
</layout:cell>
</layout:panelGrid>
</w:form>
</div>
</w:page>
</f:view>
![]() |
指定id,默认生成的组件是 textField,它的具体值依赖于IoVC绑定 |
![]() |
指定id,此处生成的组件是 combo,它的的具体值依赖于IoVC绑定 |
![]() |
指定id,此处生成的组件是 numberField,它的具体值依赖于IoVC绑定 |
![]() |
指定id,此处生成的组件是 dateField,它的具体值依赖于IoVC绑定 |
再来看一下后台Bean:
TestSmartWidget4Bean:
@ManagedBean(name="testSmartWidget4Bean", scope=ManagedBeanScope.SESSION)
public class TestSmartWidget4Bean implements Serializable {
@Bind(id="name")
private String name;
@Bind(id="date")
private Date date;
@Bind(id="age")
private int age;
@Bind(id="gender")
private String gender = "male";
@Bind(id="genders")
@SelectItems
private SelectItem[] genders = new SelectItem[] {
new SelectItem("male", "男"),
new SelectItem("female", "女")};
@Action(id="ok")
public void ok(){
System.out.println("name=" + name + ", date=" + date + ", age=" + age + ", gender=" + gender);
}
}
![]() |
通过IoVC将值绑定到组件 |
![]() |
通过IoVC将值绑定到组件 |
![]() |
通过IoVC将值绑定到组件 |
![]() |
通过IoVC将值绑定到组件,它是 combo 的选择值 |
![]() |
通过IoVC将值绑定到组件,它是 combo 下拉列表内容所依赖的 |
运行效果如下:

事实上,该复合组件的衍化过程还可以继续,以让 SmartWidget 更加完美的适应你的需求,如:声明一个全局的 width 值,后续所有 SmartWidget 的 width 都依赖于此全局变量,等等。程序的衍化之路是不间断的,但本文却不得不结束了。
另外需要提及的是,本文中,用到大量的ELite特性,而 ELite 带给 AOM 远不只这些,如果你有兴趣,可以访问 www.operamasks.org 。
一点想法
讲得很好。SmartWidget的第四次衍化中如果能用动态脚本拼凑代码的话就可以减少很多工作量了。
因为都是 <w:XXXX YYYY* > ZZZZ? </w:XXXX> 这样的格式,
XXXX其实就是#{type}
ZZZZ为#{combo.equals(type)? "" : "<f:selectItems id=#{selectItemsId}/>"
YYYY都是id="#{id}" value="#{value}" width="#{widgetWidth}" 这三者的组合,与type无关。
如果能用像javascript一样动态拼凑一个 <w:XXXX YYYY* > ZZZZ? </w:XXXX> 这样的代码出来的话,工作量就少很多。
可惜js只能拼凑成最终的html代码,不能交由AOM再处理。(意思是说实际是先AOM处理,后执行js。如果能先js后AOM再处理的话就行了)
复用是简化程序的好方法
这个复用组件的思路还是很清晰的,简单易懂,但是第四次的代码看起来确实太繁锁的,应该再简化,不然还是不实用。 而且,如果我没记错的话,AOM2.1,textfield之类的组件已经自带lable属性了
如何做可包含子组件的复合组件?
你上面讲的那些例子做出来的组件都是不可包含子组件的. 如果我要定义一个类似w:dataGrid一样的组件怎么做? 如 <my:datagrid rows="10" $gt; //这里可以自己任意添加w:outputColumn </my:datagrid rows="10" $gt; 就是说我做出的复合组件还可以由大家任意添加允许的子组件,这种需求像一个大系统中要统一风格而且减少大家工作量时需要.
<tag-name>smartWidget1</tag-name>
<source>smartWidget1.xhtml</source>
<my:smartWidget4 label="注册日期:" id="date" width="80,150" type="dateField"/>
@Bind(id="genders")
一点想法
讲得很好。SmartWidget的第四次衍化中如果能用动态脚本拼凑代码的话就可以减少很多工作量了。 因为都是 这样的格式,
XXXX其实就是#{type}
ZZZZ为#{#{selectItemsId}/>"
YYYY都是id="#{id}" value="#{value}" width="#{widgetWidth}" 这三者的组合,与type无关。
如果能用像javascript一样动态拼凑一个 这样的代码出来的话,工作量就少很多。
可惜js只能拼凑成最终的html代码,不能交由AOM再处理。(意思是说实际是先AOM处理,后执行js。如果能先js后AOM再处理的话就行了)
combo.equals(type)? "" : "