系列文章目录
Geoserver源码解读三 GeoServerBasePage
前言
看过前面几篇文章的朋友应该知道,Geoserver是使用Spring+Wicket的,本文就以图层预览界面添加属性表的例子,看下geoserver是怎么利用Spring和Wicket优雅的扩展新页面或者模块的
一、要实现什么功能
打开图层预览界面,默认的样子是这样的
联想到好多桌面端GIS软件(比如Arcgis、QGIS、超图)都有一个功能叫“打开属性表”,也就是说查看矢量图层的表数据,方便我们预览矢量图层里面到底有什么东西,于是乎突发奇想,想要再geoserver的图层预览界面添加一个查看属性表的功能
查看属性表的页面大概长这样
二、Geoserver的插件化开发模式
1.插件化开发模式的好处
安装过geoserver插件的朋友应该知道,只要吧插件的jar包放到geoserver安装目录的lib文件夹下就可以,不需要其它的任何操作,简单、
lib文件夹目录:安装目录\GeoServer\lib
2.怎么实现的
为什么它能做到那么优雅的安装插件呢,这就是spring这个大管家的功劳了,抽丝剥茧的看下geoserver的菜单目录加载模式
在GeoServerBasePage.html代码中看到起菜单的变量是category.links
org/geoserver/web/GeoServerBasePage.html
<li class="navigation-tab" wicket:id="category">
<div class="navigation-tab-header">
<span wicket:id="category.header">[Category Header]</span>
</div>
<ul class="navigation-tab-content plain">
<li class="nav-administer-service" wicket:id="category.links">
<a wicket:id="link">
<img src="#" wicket:id="link.icon"/><span wicket:id="link.label"></span>
</a>
</li>
</ul>
</li>
其对应的java代码是
List<MenuPageInfo<GeoServerBasePage>> infos =
(List) filterByAuth(getGeoServerApplication().getBeansOfType(MenuPageInfo.class));
final Map<Category, List<MenuPageInfo<GeoServerBasePage>>> links = splitByCategory(infos);
List<MenuPageInfo<GeoServerBasePage>> standalone =
links.containsKey(null) ? links.get(null) : new ArrayList<>();
links.remove(null);
List<Category> categories = new ArrayList<>(links.keySet());
重点在这行代码
(List) filterByAuth(getGeoServerApplication().getBeansOfType(MenuPageInfo.class))
它的作用是获取所有MenuPageInfo类型的java类
在 spring的 applicationContext.xml里面能看到各种MenuPageInfo类型的bean,比如这个
<bean id="wmsServicePage" class="org.geoserver.web.services.ServiceMenuPageInfo">
<property name="id" value="wms"/>
<property name="titleKey" value="wms.title"/>
<property name="descriptionKey" value="wms.description"/>
<property name="componentClass" value="org.geoserver.wms.web.WMSAdminPage"/>
<property name="icon" value="server_map.png"/>
<property name="category" ref="servicesCategory"/>
<property name="serviceClass" value="org.geoserver.wms.WMSInfo"/>
</bean>
也就是说只要在spring中注册了MenuPageInfo类型的java类,都会被获取到,不管是哪个包下面的
以此类推就明白了Geoserver是怎么优雅的加载插件或者扩展项目了
三、图层预览页面逻辑
打开图层预览页面的html代码
代码位置:org/geoserver/web/demo/MapPreviewPage.html
<!-- The fragment for the common links -->
<wicket:fragment wicket:id="commonLinks">
<span style="white-space: nowrap;" wicket:id="commonFormat">
<a target="_blank" href="#" wicket:id="theLink">theTitle</a>
</span>
</wicket:fragment>
再看它对应的java代码
private List<ExternalLink> commonFormatLinks(PreviewLayer layer) {
List<ExternalLink> links = new ArrayList<>();
List<CommonFormatLink> formats =
getGeoServerApplication().getBeansOfType(CommonFormatLink.class);
Collections.sort(formats);
for (CommonFormatLink link : formats) {
links.add(link.getFormatLink(layer));
}
return links;
}
能看出来,它和上面加载菜单的是一个套路,都用到了获取所有某类型的javaBean
getGeoServerApplication().getBeansOfType
那么如果想要加一个属性表的常用格式链接,就需要创建一个CommonFormatLink类型的javaBean
四、属性表连接添加
方式一、直接在源码中添加
参照KMLFormatLink.java,我在这个位置建了个javaBean
org/geoserver/web/demo/PropertySheetFormatLink.java
public class PropertySheetFormatLink extends CommonFormatLink {
@Override
public ExternalLink getFormatLink(PreviewLayer layer) {
ExternalLink olLink =
new ExternalLink(
this.getComponentId(),
//这个位置临时写成百度的地址,后期再改
"http://www.baidu.com",
(new StringResourceModel(this.getTitleKey(), null, null)).getString());
olLink.setVisible(layer.getType() == PreviewLayer.PreviewLayerType.Vector && layer.hasServiceSupport("WFS"));
return olLink;
}
}
然后再配置文件applicationContext.xml中注册该javaBean
<bean id="propertySheetPreview" class="org.geoserver.web.demo.PropertySheetFormatLink">
<property name="id" value="propertySheet"/>
<property name="titleKey" value="propertySheet.title"/>
<property name="order" value="40"/>
</bean>
能看到上面有个titleKey的属性,需要在配置文件中给赋下值(上篇文章的i18n有讲到这个)
在通用包GeoServerApplication.properties中添加
propertySheet.title=PropertySheet
在中文包GeoServerApplication_zh.properties中添加(中文需要改为ISO-8859编码)
propertySheet.title=\u5c5e\u6027\u8868
方式二、在新建模块中添加
虽说属性表的功能能在源码上直接改,但可扩展性就降低了,别人的geoserver想用你的功能就很难,所以更推荐新建一个模块,作为一个插件去开发。在上篇文章中讲到了geoserver插件的一些东西的存储位置在src下面的extension文件夹中,也可以说是extension模块中,我也按照它的思路去在这儿建一个模块
建好模块后在主模块【src/web/app】的pom文件中添加一个profile预设
<profile>
<id>vector-plugin</id>
<dependencies>
<dependency>
<groupId>org.geoserver.extension</groupId>
<artifactId>vector-plugin</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</profile>
添加预设后就能maven中看到了,勾选的操作就相当于把插件作为一个模块引入到主模块了
如果你那儿爆红或者没有出现这个复选框,就需要重新构建下你新建或者修改的模块
重起项目后就能看到这个属性表的超链接了
需要注意的是这个连接类型【ExternalLink】是定死的,意思是在新的窗口打开这个连接,相当于“window.open('https://www.example.com', '_blank');”。这点儿就很难受,本来想使用其他页面的【BookmarkablePageLink】类型的连接在当前页面显示,不过人家既然这样限制了,估计geoserver官方有它的理由吧。
五、属性表页面设计
看了geoserver源码后,发现它的页面大致分为三类
类型 | |
静态页面 | geoserver文件目录的【www】位置,可以直接访问http://localhost:8080/geoserver/www/ol-demo.html |
Wicket | 大多数页面使用的方式,是 GeoServer Web 界面的核心, |
FreeMarker | 一个模板引擎,多用于生成页面的动态内容,有点像接口 |
单纯的静态页面就没有学习的必要了,这里暂时选择用Wicket 去实现属性表的页面,如果要使用Wicket实现的话有个小问题需要注意下,ExternalLink 是专门用于外部链接的,因此直接在 ExternalLink 中引用 Wicket 页面可能会遇到一些问题。如果需要在 Wicket 页面中进行页面间的跳转,通常会使用 BookmarkablePageLink 或 PageLink 组件,但是此处是人家geoserver要求的让用ExternalLink 为了尽可能保持Geoserver优雅的风格,在上面第四章建的PropertySheetFormatLink.java中只能给转换下
// 生成目标页面的 URL
String targetPageUrl = getRequestCycle().urlFor(PropertySheetPage.class, null).toString();
// 创建 ExternalLink
ExternalLink externalLink = new ExternalLink(this.getComponentId(), targetPageUrl, (new StringResourceModel(this.getTitleKey(), null, null)).getString());
上面代码的PropertySheetPage.class 就是要即将创建的Wicket 页面类
1.html页面设计
代码位置:src/extension/vector-plugin/src/main/java/org/geoserver/vector/preview/PropertySheetPage.html
<html xmlns:wicket="http://wicket.apache.org/">
<head>
<title><wicket:message key="propertySheet.title"></wicket:message></title>
<wicket:head>
<meta http-equiv="X-UA-Compatible" content="IE=10" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<wicket:link>
<link rel="stylesheet" href="css/geoserver.css" type="text/css" media="screen, projection" />
</wicket:link>
</wicket:head>
</head>
<body>
<wicket>
<!-- the table component -->
<div wicket:id="table"></div>
<!-- The fragment for the icon -->
<wicket:fragment wicket:id="iconFragment">
<img wicket:id="layerIcon"/>
</wicket:fragment>
</wicket>
</body>
</html>
需要注意的是因为这个页面是独立存在的,所以默认没有和GeoServerBasePage共享css,所以我重新拷贝了一个geoserver.css,这样就能尽可能一样的和原页面风格保持一致
2.对应java类代码
首相要简单讲一下Wicket 页面的两个核心概念和设计模式,从而更好的理解Geoserver烦人源码
2.1 Wicket 两大核心概念
- 数据提供者(DataProvider)
数据提供者是一个接口,它定义了如何获取数据集合。数据提供者通常用于复杂的数据操作,如分页、排序和过滤。Wicket 提供了多种数据提供者的实现,如 ListDataProvider
、SortableDataProvider
、FilterableDataProvider
等。
数据提供者不直接与 Wicket 组件绑定,而是通过模型(Model)来提供数据。数据提供者通常用于复杂的数据操作,而模型用于简单地绑定数据到组件。
- 模型(Model)
模型是一个接口,它定义了如何获取和设置数据。Wicket 提供了多种模型实现,如 CompoundPropertyModel
、MapModel
、LoadableDetachableModel
等。
模型通常与 Wicket 组件绑定,提供组件的数据。模型可以是简单的,也可以是复杂的,取决于你的应用需求。
2.2 Wicket 设式计模
在设计 Wicket 应用程序时,通常会使用以下模式:
-
单数据源:每个页面或组件通常有一个数据源,它可以是一个数据提供者或一个模型。
-
模型和数据提供者的分离:通常情况下,模型负责绑定数据到组件,而数据提供者负责提供数据。模型可以是一个简单的
Model
,而数据提供者可以是更复杂的SortableDataProvider
。 -
数据绑定:使用 Wicket 的数据绑定特性,你可以轻松地将模型或数据提供者绑定到组件,如表格、表单等。
-
状态管理:在 Wicket 中,状态通常由模型和数据提供者来管理。你可以使用
LoadableDetachableModel
来延迟加载数据,或者使用SortableDataProvider
来提供排序的数据。
2.3 设计java类
代码位置:src/extension/vector-plugin/src/main/java/org/geoserver/vector/preview/PropertySheetPage.java
我帮大家看了下geoserver那一坨坨枯燥的代码,结合上面的两个概念和设计模式,总结出Geoserver主要是使用上面的第2中设计模式模型和数据提供者的分离,也就是说创建一个Provider,然后在Provider中创建一个Model,然后此处也使用这种方案,现在需要创建的有四个类
FeatureAttributionModel.java | Model |
PropertySheetProvider.java | Provider |
FeatureAttributionInfoImpl.java | 可序列化的Feature实体类 |
PropertySheetPage.java | 主页面类 |
2.3.1 FeatureAttributionInfoImpl
这是一个可序列化的要素实体类
// interface
public interface FeatureAttributionInfo extends Serializable {
SimpleFeature getSimpleFeature();
}
// 实现类(通常和上面的interface 分两个文件存储)
public class FeatureAttributionInfoImpl implements FeatureAttributionInfo {
protected transient SimpleFeature simpleFeature;
public FeatureAttributionInfoImpl(SimpleFeature simpleFeature){
this.simpleFeature = simpleFeature;
}
@Override
public SimpleFeature getSimpleFeature() {
return this.simpleFeature;
}
}
没来没有打算建这个类,创建表格组件的时候想着直接用SimpleFeature就行
// 初步方案
GeoServerTablePanel<SimpleFeature> table;
table = new GeoServerTablePanel<SimpleFeature>("table", provider){}
// 后期优化
GeoServerTablePanel<FeatureAttributionInfo > table;
table = new GeoServerTablePanel<FeatureAttributionInfo >("table", provider){}
但是使用之后就一直报错,说是wiket的数据一般是用于交互的所以必须实现Serializable
接口,也就是说必须可序列化的,而SimpleFeature不实现Serializable
接口,跟了下源码,发现是这个地方做了限制(GeoServerDataProvider.java)
public interface Property<T> extends Serializable
所以就只能再定义一个可序列化的中间类
2.3.2 FeatureAttributionModel
这是个Model类,实际上没有太多东西只用于传值
class FeatureAttributionModel extends LoadableDetachableModel<FeatureAttributionInfo> {
FeatureAttributionInfo featureAttributionInfo;
public FeatureAttributionModel(FeatureAttributionInfo pl) {
super(pl);
featureAttributionInfo = pl;
}
@Override
protected FeatureAttributionInfo load() {
return featureAttributionInfo;
}
}
2.3.3 PropertySheetProvider
继承GeoServerDataProvider<FeatureAttributionInfo>,用于通用的表格
public class PropertySheetProvider extends GeoServerDataProvider<FeatureAttributionInfo> {
public PropertySheetProvider(String layerName) {
super();
this.layerName = layerName;
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
cache = builder.expireAfterWrite(DEFAULT_CACHE_TIME, TimeUnit.SECONDS).build();
// Callable which internally calls the size method
sizeCaller = new SizeCallable();
// Callable which internally calls the fullSize() method
fullSizeCaller = new FullSizeCallable();
initPropertyList();
}
}
里面有一些关键的代码我列了出来
1.初始化属性表的,用于处理表格的列和属性表字段的对应关系
/**
* 初始化属性字段列表
*/
protected void initPropertyList(){
final Catalog catalog = getCatalog();
this.propertyList.clear();
LayerInfo currentLayerInfo = catalog.getLayerByName(this.layerName);
ResourceInfo resourceInfo = currentLayerInfo.getResource();
List<AttributeDescriptor> attributeDescriptors = null;
// 从代理模式中获取到原值
if (Proxy.isProxyClass(resourceInfo.getClass())) {
if(Proxy.getInvocationHandler(resourceInfo) instanceof ModificationProxy){
FeatureTypeInfoImpl featureTypeInfo = (FeatureTypeInfoImpl)ModificationProxy.handler(resourceInfo).getProxyObject();
try {
attributeDescriptors = ((SimpleFeatureType) featureTypeInfo.getFeatureType()).getAttributeDescriptors();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}else {
SimpleFeatureType simpleFeatureType = null;
try {
simpleFeatureType = (SimpleFeatureType) ((SecuredFeatureTypeInfo) resourceInfo).getFeatureType();
} catch (IOException e) {
throw new RuntimeException(e);
}
attributeDescriptors = simpleFeatureType.getAttributeDescriptors();
}
// 遍历属性描述符数组,为每个属性创建列并添加到列集合中
for (AttributeDescriptor descriptor : attributeDescriptors) {
if(!(descriptor instanceof GeometryDescriptorImpl)){
// 获取属性名称及其类型
String name = descriptor.getLocalName();
this.propertyList.add(new AbstractProperty<FeatureAttributionInfo>(name) {
@Override
public Object getPropertyValue(FeatureAttributionInfo item) {
return item.getSimpleFeature().getProperties(name).iterator().next().getValue();
}
});
}
}
}
2.查询迭代器
@Override
public Iterator<FeatureAttributionInfo> iterator(final long first, final long count) {
SimpleFeatureCollection simpleFeatureIterator = null;
try {
simpleFeatureIterator = filteredItems(first, count);
} catch (IOException e) {
throw new RuntimeException(e);
}
SimpleFeatureCollection finalSimpleFeatureIterator = simpleFeatureIterator;
return new Iterator<FeatureAttributionInfo>() {
@Override
public boolean hasNext() {
return finalSimpleFeatureIterator.features().hasNext();
}
@Override
public FeatureAttributionInfo next() {
SimpleFeature simpleFeature = finalSimpleFeatureIterator.features().next();
FeatureAttributionInfo featureAttributionInfo = new FeatureAttributionInfoImpl(simpleFeature);
return featureAttributionInfo;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove operation is not supported");
}
};
}
2.3.4 PropertySheetPage
主类使用Provider从而实现功能
PropertySheetProvider provider;
GeoServerTablePanel<FeatureAttributionInfo> table;
// private transient List<String> availableWFSFormats;
public PropertySheetPage(final PageParameters parameters) {
// 从PageParameters中获取layerName
String layerName = parameters.get("layerName").toString();
provider = new PropertySheetProvider(layerName);
// build the table
table =
new GeoServerTablePanel<FeatureAttributionInfo>("table", provider) {
private static final long serialVersionUID = 1L;
@Override
protected Component getComponentForProperty(
String id,
IModel<FeatureAttributionInfo> itemModel,
Property<FeatureAttributionInfo> property) {
return new Label(id, property.getModel(itemModel));
}
};
table.setOutputMarkupId(true);
add(table);
}
总结
其实总体就是参照图层预览页面,然后做的一些改造,最终的成果虽说实现了功能,但还是有点儿小问题,页面初始加载的可慢,我暂时还没发现问题出在哪个地方,后期发现了我再回来补充,如果哪个大佬知道什么原因,也欢迎在评论区留言。