系列文章目录
Geoserver源码解读一 环境搭建
Geoserver源码解读二 主入口
Geoserver源码解读三 GeoServerBasePage
Geoserver源码解读四 REST服务
Geoserver源码解读五 Catalog
Geoserver源码解读六 插件(怎么在开发模式下使用)
目录
系列文章目录
前言
一、要实现什么功能
二、Geoserver的插件化开发模式
1.插件化开发模式的好处
2.怎么实现的
三、图层预览页面逻辑
四、属性表连接添加
方式一、直接在源码中添加
方式二、在新建模块中添加
五、属性表页面设计
1.html页面设计
2.对应java类代码
2.1 Wicket 两大核心概念
2.2 Wicket 设式计模
2.3 设计java类
编辑
2.3.1 FeatureAttributionInfoImpl
2.3.2 FeatureAttributionModel
2.3.3 PropertySheetProvider
2.3.4 PropertySheetPage
总结
前言
看过前面几篇文章的朋友应该知道,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);
}
总结
其实总体就是参照图层预览页面,然后做的一些改造,最终的成果虽说实现了功能,但还是有点儿小问题,页面初始加载的可慢,我暂时还没发现问题出在哪个地方,后期发现了我再回来补充,如果哪个大佬知道什么原因,也欢迎在评论区留言。