# portal和业务空间
# 一、什么是portal?
官方定义:Portal是一种Web应用,通常用来提供个性化、单点登录、聚集各个信息源的内容,并作为信息系统表现层的宿主。聚集是指将来自各个信息源的内容集成到一个Web页面里的活动。
百度解释:portal意门户,现多用于互联网的门户(入口)网站和企业应用系统的门户系统。
1)广义注解
这里是一个应用框架,它将各种应用系统、数据资源和互联网资源集成到一个信息管理平台之上,并以统一的用户界面提供给用户,使企业可以快速地建立企业对客户、企业对内部员工和企业对企业的信息通道, 使企业能够释放存储在企业内部和外部的各种信息。 它将各种应用系统、数据资源和互联网资源集成到一个信息管理平台之上,并以统一的用户界面提供给用户,使企业可以快速地建立企业对客户、企业对内部员工和企业对企业的信息通道, 使企业能够释放存储在企业内部和外部的各种信息。
2)狭义注解
所谓门户网站(入口网站),是指通向某类综合性互联网信息资源并提供有关信息服务的应用系统。门户网站最初提供搜索引擎和网络接入服务,后来由于市场竞争日益激烈,门户网站不得不快速地拓展各种新的业务类型,希望通过门类众多的业务来吸引和留驻互联网用户,以至于目前门户网站的业务包罗万象,成为网络世界的“百货商场”或“网络超市”。从现在的情况来看,门户网站主要提供新闻、搜索引擎、网络接入、聊天室、电子公告牌(BBS)、免费邮箱、影音资讯、电子商务、网络社区、网络游戏、免费网页空间,等等。在中国,典型的门户网站有新浪网、网易和搜狐网等;台湾的著名入口网站则是以雅虎奇摩(Yahoo-Kimo)、蕃薯藤(Yam)、网路家庭(PChome)等。
从定义中我们可知portal的三个特点:
a. Personalization (个性化):用户可以自己定制自己所需要的页面;
b. Single sign on(单点登陆):一处登陆,处处通行;
c. Content aggregation(内容聚合):不同来源的信息整合到一个页面中 。
# 二、什么是业务空间?
业务空间是portal的侠义体现,通过提供布局设置,栏目配置,数据源配置等机制实现客户可以定制化页面的需求,现阶段实现了portal的两个点:
a. Personalization (个性化):用户可以自己定制自己所需要的页面;
通过布局设置,栏目配置等实现.
b. Single sign on(单点登陆):一处登陆,处处通行;
暂时无此业务需求,未实现.
c. Content aggregation(内容聚合):不同来源的信息整合到一个页面中 。
通过数据源开发和配置可以集成当前系统和其他系统的信息.
# 三、业务空间渲染过程
# 四、业务空间与JSR-168
JSR-168是适合于portlet开发人员的Java API集合,JSR286是JSR168的增强版,对JSR168向后兼容。在JSR-168/JSR-268中将页面分为三个部分:1)Portal Server, 2) Portlet Container , 3) Portlet
1) Portal Server
一个Portal(门户网站)就是指一个Web-based的系统,通常都会提供个性化设置,单一登陆、以及由各种不同来源或者网站取得各式各样的信息,并且将这些信息放在网页之中组合而成的呈现平台,门户网站会由精巧的个性化设置去提供定制的网页,当不同等级的使用者来浏览该页面将获得不同的信息内容。
2) Portlet Container
Portlet Container 是提供portlets执行的环境,包含了很多porlets并管理他们的生命周期,他也会保存着portlets的喜好设置,一个portlet container接收到来自portal的请求后,接着将这个请求传递给存在container的portlet执行,portlet container没有义务去组合portlets产生的信息内容,这个工作必须由portal来处理,portal和portlet和portlet container可以放在一起视为一个系统的组件,或者分开为两个独立的组件。
3) Portlet
一个Portlet是以Java技术为基础的Web组件,由Portlet Container所管理,专门处理客户的request以及产生各种动态的信息内容。Portlets分为可拔插(pluggable)的客户界面组件,提供呈现层成为一个信息系统。
这些由portlet产生的内容称为片段(fragment),而片段是具有一些规则的Markup(HTML, XHTML, WML),而且可以和其他的片段组合为一个复杂的文件。而Portlet中的内容正常来说是与其他Portlet的内容聚合而成为一个Portal网页。而Porttlet的生命周期是被Portlet Container所管理控制的。
客户端和portlets的互动是由portal通过典型的request/response方式实现,正常来说,客户回和portlets所产生的内容互动,举例来说,根据下一步的连接或者确认送出的form请求,结果portal将回接收到portlet的动作,将这个处理状态转向目标的portlet。这些portlet内容的产生可能会因为不同的使用者而有不同的变化,完全是根据客户对于这个portlet的设置。
# JSR-168参考实现
a) Spring Portlet MVC (opens new window)
以Spring Portlet MVC开发示例,以下摘自Spring Portlet MVC开发入门示例 (opens new window)
Spring Portlet MVC和其Web MVC可以说是如出一辙,只是在Web MVC中处于核心的DispatcherServlet在Portlet MVC中换成了DispatcherPortlet,如下图描述了Portlet request是如何被处理的.关于Spring 的Web MVC,请参照 http://blog.csdn.net/kkdelta/article/details/7274708 (opens new window)
DispatcherPortlet配置在portlet.xml文件中,它继承了Portlet标准中的GenericPortlet,所以它本质上是一个能够将Portlet Request dispatch到Spring框架中其它MVC组件的一个Portlet.配置如下:
<portlet>
<portlet-name>helloWorld</portlet-name>
<portlet-class>org.springframework.web.portlet.DispatcherPortlet</portlet-class>
<supports>
<mime-type>text/html</mime-type>
<portlet-mode>view</portlet-mode>
</supports>
<resource-bundle>content.Language-ext</resource-bundle>
<portlet-info>
<title>Hello World</title>
</portlet-info>
</portlet>
这里以Render Request处理为例,当DispatcherPortlet接收到Request的时候,它会根据handermapping的配置找到相应的Controler来处理请求.Controler处理完后返回一个ModelAndView,对于View的处理则和Web MVC类似了,这里不再做介绍.
<bean id="helloWorldController"
class="chapter07.code.listing.HelloWorldController"></bean>
<bean id="portletModeHandlerMapping"
class="org.springframework.web.portlet.handler.PortletModeHandlerMapping">
<property name="portletModeMap">
<map>
<entry key="view">
<ref bean="helloWorldController" ></ref>
</entry>
</map>
</property>
</bean>
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView" ></property>
<property name="prefix" value="/WEB-INF/jsp/" ></property>
<property name="suffix" value=".jsp" ></property>
</bean>
public class HelloWorldController implements Controller { public void handleActionRequest(ActionRequest request, ActionResponse response) throws Exception { //-- do nothing the Hello World portlet doesn't receive //-- action requests. }
public ModelAndView handleRenderRequest(RenderRequest request, RenderResponse response) throws Exception { Map<String, Object> model = new HashMap<String, Object>(); model.put("helloWorldMessage", "Hello World"); return new ModelAndView("helloWorld", model); } } 本文的例子参照了Portlets in Action,完整代码可以从http://download.csdn.net/detail/kkdelta/4125924下载 (opens new window). 这个例子可以运行在Liferay的Portal server上,关于Portlet和LifeRay的介绍可以参考 http://blog.csdn.net/kkdelta/article/category/1082877 (opens new window)
Spring的Controller提供了处理Render request和Action request的方法,对于处理Event和resource类型的request可以分别实现EventAwareController和ResourceAwareController.
同时通过注解的方式也可以mapping响应的request到具体的方法进行处理.Spring提供了@Controller,@RenderMapping和@ActionMapping等等.
# 业务空间是否符合JSR-168规范?
业务空间并不符合JSR-168规范。JSR-168是一个Portal在Java API参考,JSR-168的三个组件(Portal Server,Portlet Container , Portlet)我们并没有参考其API去做实现。
# 五、业务空间与V5空间
我们先看下jquery-ui-portal的截图
大家是否有发现如果仅仅从布局和功能上,jquery-ui-portal与我们的门户(V5门户和业务空间)大体一样。
# 5.1 V5门户是一个非常典型和完整的Portal实现
包含以下功能特点:
a. Personalization (个性化):用户可以自己定制自己所需要的页面;
b. Single sign on(单点登陆):一处登陆,处处通行;
c. Content aggregation(内容聚合):不同来源的信息整合到一个页面中 ;
d. 消息通知。
# 5.2 V5门户开发栏目过程
V5门户采用针对业务场景制作一个栏目,所以V5门户采用开发栏目的方式,可以将各个系统的信息整合到V5门户页面上来,栏目开发包含以下内容:
a) 后端开发一个类BannerSection并继承BaseSectionImpl;
::: demo
public class BannerSection extends BaseSectionImpl {
@Override
public String getResolveFunction(Map<String, String> preference) {
return BannerTemplete.RESOLVE_FUNCTION;
}
@Override
public String getIcon() {
return null;
}
@Override
public String getId() {
return "banner";
}
public boolean isNoHeaderSection() {
return true;
}
@Override
public String getBaseName() {
return ResourceUtil.getString("space.section.banner");
}
@Override
public String getBaseNameI18nKey(){
return "space.section.banner";
}
@Override
public String getName(Map<String, String> preference) {
return ResourceUtil.getString("space.section.banner");
}
@Override
public Integer getTotal(Map<String, String> preference) {
return null;
}
@Override
public BaseSectionTemplete projection(Map<String, String> preference) {
BannerTemplete bannerHtml= this.getHTML(preference);
return bannerHtml;
}
public BannerTemplete getHTML(Map<String, String> preference) {
int columnsStyle = this.getSectionProperty(0, preference, "columnsStyle");
String textAlign = "left";
if(columnsStyle == 3){
textAlign = "center";
} else if(columnsStyle == 4){
textAlign = "right";
}
int height = this.getSectionProperty(60, preference, "height");
int fontSize = this.getSectionProperty(4, preference, "fontSize");
int fontStyle = this.getSectionProperty(1, preference, "fontStyle");
String fontColor = preference.get("fontColor");
if(Strings.isBlank(fontColor)){
fontColor= "0";
}
String backgroundColor = preference.get("backgroundColor");
if(Strings.isBlank(backgroundColor)){
backgroundColor= "";
}
if("0".endsWith(fontColor)){
fontColor= "#4c6498";
}else if("1".endsWith(fontColor)){
fontColor= "#ff0000";
}else if("2".endsWith(fontColor)){
fontColor= "#ff7c03";
}else if("3".endsWith(fontColor)){
fontColor= "#5c3d09";
}else if("4".endsWith(fontColor)){
fontColor= "#ffff00";
}else if("5".endsWith(fontColor)){
fontColor= "#00ff00";
}else if("6".endsWith(fontColor)){
fontColor= "#0000ff";
}else if("7".endsWith(fontColor)){
fontColor= "#ff00ff";
}else if("8".endsWith(fontColor)){
fontColor= "#6e6e6e";
}else if("9".endsWith(fontColor)){
fontColor= "#000000";
}
String backgroundRepeat = preference.get("backgroundTile");
if(Strings.isBlank(backgroundRepeat)){
backgroundRepeat = "inherit";
}
String slogan = preference.get("slogan");
if (slogan==null) {
if (ProductEditionUtil.isG6Verson(ProductEditionEnum.getCurrentProductEditionEnum().getKey() + "")) {
slogan= "";
}else{
slogan = Constants.getSloganKey();
}
}
slogan = Functions.toHTML(ResourceUtil.getString(slogan));
String resSuff = Functions.resSuffix();
String banner = preference.get("background");
String defaultBanner = "/apps_res/v3xmain/images/banner/space_banner.gif";
// String defaultBanner = "none";
if (Strings.isBlank(banner)) {
banner = defaultBanner;
} else if (!defaultBanner.equals(banner)) {
String[] banners = banner.split(",");
if (banners.length > 0) {
String bannerUrl= banners[0];
if(Strings.isNotBlank(bannerUrl) && Strings.isDigits(bannerUrl)){
banner = "/fileUpload.do?method=showRTE&fileId=" + banners[0] + "&type=image";
resSuff = "";
}else{
banner = bannerUrl;
}
}
}
BannerTemplete bannerTemplete= new BannerTemplete();
bannerTemplete.setBanner(banner);
bannerTemplete.setSlogan(slogan);
bannerTemplete.setBackgroundColor(backgroundColor);
bannerTemplete.setBackgroundRepeat(backgroundRepeat);
bannerTemplete.setFontColor(fontColor);
bannerTemplete.setFontSize(fontSize+"");
bannerTemplete.setFontStyle(fontStyle+"");
bannerTemplete.setHeight(height+"");
bannerTemplete.setTextAlign(textAlign);
bannerTemplete.setResSuff(resSuff);
bannerTemplete.setMarqueeStyle(columnsStyle+"");
return bannerTemplete;
}
public String getHTML(String entityId, String ordinal, String spaceType, String ownerId, Long spaceId) {
Map<String, String> preference = getPrefenerce(entityId, ordinal, spaceType, ownerId, null, null, null);
return null;
}
private int getSectionProperty(int defaultValue, Map<String, String> preference, String property) {
String value = preference.get(property);
if (Strings.isNotBlank(value)) {
return NumberUtils.toInt(value);
}
return defaultValue;
}
@Override
public int getHeight(Map<String, String> preference) {
String heightStr = preference.get(PropertyName.height.name());
int height = 60;
if (NumberUtils.isDigits(heightStr)) {
height = Integer.parseInt(heightStr);
}
return height;
}
}
:::
b) 在spring配置文件中加入BannerSection配置;
:::demo
<!-- 横幅 -->
<bean id="banner" class="com.seeyon.ctp.portal.section.BannerSection">
<property name="sectionType" value="common" ></property>
<property name="sortId" value="1004" ></property>
<property name="spaceTypes" value="personal,personal_custom,leader,outer,department,custom,corporation,public_custom,group,public_custom_group,cooperation_work,form_application,edoc_manage,objective_manage,meeting_manage,performance_analysis,related_project_space,big_screen,before_login,v_report" ></property>
<property name="resourceBundle" value="com.seeyon.v3x.main.resources.i18n.MainResources" ></property>
<property name="properties">
<list>
<bean class="com.seeyon.ctp.portal.section.SectionPropertyImpl">
<property name="reference">
<list>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="height" ></property>
<property name="subject" value="cannel.data.height" ></property>
<property name="valueType" value="2" ></property>
<property name="validate" value="isInteger" ></property>
<property name="validateValue" value="max=2000 min=10" ></property>
<property name="defaultValue" value="60" ></property>
<!-- 高度帮助图片 -->
<!--<property name="helpType" value="1" ></property> -->
<!--<property name="helpValue" value="help-height.png" ></property> -->
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="slogan" ></property>
<property name="subject" value="cannel.data.word" ></property>
<property name="valueType" value="2" ></property>
<property name="validate" value="maxLength" ></property>
<property name="validateValue" value="maxSize=600" ></property>
<property name="editSuffix" value="EditionSuffix" ></property>
<property name="defaultValue" value="space.label.slogan.default" ></property>
<property name="hiddenValue">
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="slogan" ></property>
<property name="editSuffix" value="EditionSuffix" ></property>
<property name="defaultValue" value="space.label.slogan.default" ></property>
</bean>
</property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="columnsStyle" ></property>
<property name="subject" value="cannel.wordstyle.label" ></property>
<property name="valueType" value="0" ></property>
<property name="defaultValue" value="0" ></property>
<property name="valueRanges">
<list>
<bean class=" com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="columnsStyle.0.label" ></property>
<property name="value" value="0" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="columnsStyle.3.label" ></property>
<property name="value" value="3" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="columnsStyle.4.label" ></property>
<property name="value" value="4" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="columnsStyle.1.label" ></property>
<property name="value" value="1" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="columnsStyle.2.label" ></property>
<property name="value" value="2" ></property>
</bean>
</list>
</property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="fontSize" ></property>
<property name="subject" value="cannel.data.fontSize" ></property>
<property name="valueType" value="0" ></property>
<property name="defaultValue" value="2" ></property>
<property name="valueRanges">
<list>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="fontSize.0.label" ></property>
<property name="value" value="0" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="fontSize.1.label" ></property>
<property name="value" value="1" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="fontSize.2.label" ></property>
<property name="value" value="2" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="fontSize.3.label" ></property>
<property name="value" value="3" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="fontSize.4.label" ></property>
<property name="value" value="4" ></property>
</bean>
</list>
</property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="fontStyle" ></property>
<property name="subject" value="cannel.data.fontStyle" ></property>
<property name="valueType" value="0" ></property>
<property name="defaultValue" value="1" ></property>
<property name="valueRanges">
<list>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="fontStyle.0.label" ></property>
<property name="value" value="0" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="fontStyle.1.label" ></property>
<property name="value" value="1" ></property>
</bean>
</list>
</property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="fontColor" ></property>
<property name="subject" value="cannel.data.fontColor" ></property>
<property name="valueType" value="11" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="backgroundColor" ></property>
<property name="subject" value="cannel.data.backgroundColor" ></property>
<property name="valueType" value="11" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="background" ></property>
<property name="subject" value="section.name.banner.background" ></property>
<property name="valueType" value="4" ></property>
<property name="defaultValue" value="/apps_res/v3xmain/images/banner/space_banner.gif" ></property>
<property name="hiddenValue">
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="bannerFileName" ></property>
<property name="defaultValue" value="/apps_res/v3xmain/images/banner/space_banner.gif" ></property>
</bean>
</property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceImpl">
<property name="name" value="backgroundTile" ></property>
<property name="subject" value="cannel.background.image.displayStyle" ></property>
<property name="valueType" value="5" ></property>
<property name="defaultValue" value="inherit" ></property>
<property name="valueRanges">
<list>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="cannel.background.tile" ></property>
<property name="readOnly" value="false" ></property>
<property name="value" value="inherit" ></property>
</bean>
<bean class="com.seeyon.ctp.portal.section.SectionReferenceValueRangeImpl">
<property name="subject" value="cannel.background.notile" ></property>
<property name="readOnly" value="false" ></property>
<property name="value" value="no-repeat" ></property>
</bean>
</list>
</property>
</bean>
</list>
</property>
</bean>
</list>
</property>
</bean>
:::
c) 开发栏目的前端部分:
i tpl-bannerTemplete.html:栏目前端模板文件;
ii tpl-bannerTemplete.js:栏目前端JS事件交互文件;
iii tpl-bannerTemplete.css:栏目前端样式文件;
# 5.3 V5门户优点
a)针对业务场景制作栏目,基本可以满足各种业务场景的信息集成需求;
b)基础栏目开发简单,一个Section后端类 + 一个前端模板文件(html/js)
# 5.4 V5门户缺点
a) V5针对业务场景制作栏目,对于前端显示模板的复用性不高,无法多个后端Section公用一套前端显示模板;
b) V5栏目机制上不满足一个Section后端类 + 多个前端模板文件;
c) V5的门户不可以随业务包导入导出;
d) V5的门户不可以通过商城增加栏目;
# 5.5 业务空间与V5门户比较
业务空间大多理念和使用来源于V5门户,然而业务空间与V5门户有着本质上的区别:
业务空间是开发数据源拓展可取数的范围,通过开发栏目拓展数据的展现方式;而V5是开发一个个前端+后端的栏目。
a) 业务空间前后端以规定的数据格式进行交互。a)前端栏目开发者面基于已确定好的数据格式开发栏目展现;b) 后端根据栏目取数参数获取数据;3)数据格式返回由平台统一指定。
b) 栏目的开发是不需要关注业务的,只需要知道数据格式及支持的操作(基于此业务空间的栏目开发可以脱离V5环境进行开发工作);
c) 由于b)的存在,业务空间在业务场景适配上无法做到V5门户那么定制化的需求。
d) 业务空间的配置(布局,栏目,数据源配置等)可以随业务包导出;
e) 业务空间可以通过商城增加栏目;