这篇文章将简单介绍下midpoint中的界面采用的技术和介绍一下用这些技术midpoint是如何做出一个table的。
1、GUI技术简介
midpoint前端使用的框架是wicket、福客户端技术采用Bootstrap框架和 AdminLTE template。
官网对其GUI的介绍可看这个网址:https://wiki.evolveum.com/display/midPoint/GUI+Development+Guide
wicket的界面编程参看wiki中的介绍更易上手,该地址为:https://cwiki.apache.org/confluence/display/WICKET/Reference+library
2、table 数据获取源码分析
本人从用户列表页进行wicket table编程的源码剖析。该页面如下
该页面的路径是:com.evolveum.midpoint.web.page.admin.users.PageUsers
该页面对于的html页面是:com/evolveum/midpoint/web/page/admin/users/PageUsers.html
在正式开始介绍midpoint列表页面前,现说一些wicket编程的规范
wicket是Java平台下一个面向组件的web应用程序开源框架。每个组件均由一个 Java 类和一个 HTML 文件组成。默认情况下需要Java类和HTML放在同一个目录下(当然也可以放在不同的目录,但是需要进行一番设置,在此不详述)。
wicket table的编程示例可以在该页面学习:https://cwiki.apache.org/confluence/display/WICKET/Simple+Sortable+DataTable+Example
从该示例教程中我们知道 wicket table编程,关键是需要有一个provider类,自己实现iterator方法为table提供数据来源
源码解析
PageUsers.html页面源码如下
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 该页面仅描述了table的代码,并为包含上图中的 页头、菜单,本篇主要介绍table,页头、菜单的源码解析后续篇章在记录
-->
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:extend>
<form wicket:id="mainForm" class="form-inline">
<div wicket:id="table"/>
</form>
</wicket:extend>
</body>
</html>
在该页面中table 创建的核心代码是
public class PageUsers extends PageAdminUsers {
private static final String ID_MAIN_FORM = "mainForm";
private static final String ID_TABLE = "table";
public PageUsers(boolean clearPagingInSession, final UsersDto.SearchType type, final String text) {
initLayout();
}
private void initLayout() {
Form mainForm = new com.evolveum.midpoint.web.component.form.Form(ID_MAIN_FORM);
add(mainForm);
initTable(mainForm);
}
private void initTable(Form mainForm) {
Collection<SelectorOptions<GetOperationOptions>> options = new ArrayList<>();
MainObjectListPanel<UserType> userListPanel = new MainObjectListPanel<UserType>(ID_TABLE,
UserType.class, TableId.TABLE_USERS, options, this) {
private static final long serialVersionUID = 1L;
@Override
protected List<IColumn<SelectableBean<UserType>, String>> createColumns() {
return PageUsers.this.initColumns();
}
@Override
protected PrismObject<UserType> getNewObjectListObject(){
return (new UserType()).asPrismObject();
}
@Override
protected IColumn<SelectableBean<UserType>, String> createActionsColumn() {
return new InlineMenuButtonColumn<SelectableBean<UserType>>(createRowActions(false), 3, PageUsers.this){
@Override
protected int getHeaderNumberOfButtons() {
return 2;
}
@Override
protected List<InlineMenuItem> getHeaderMenuItems() {
return createRowActions(true);
}
};
}
@Override
protected List<InlineMenuItem> createInlineMenu() {
return createRowActions(false);
}
@Override
protected void objectDetailsPerformed(AjaxRequestTarget target, UserType object) {
userDetailsPerformed(target, object.getOid());
}
@Override
protected void newObjectPerformed(AjaxRequestTarget target) {
navigateToNext(PageUser.class);
}
};
userListPanel.setAdditionalBoxCssClasses(GuiStyleConstants.CLASS_OBJECT_USER_BOX_CSS_CLASSES);
userListPanel.setOutputMarkupId(true);
mainForm.add(userListPanel);
}
private List<IColumn<SelectableBean<UserType>, String>> initColumns() {
List<IColumn<SelectableBean<UserType>, String>> columns = new ArrayList<>();
IColumn<SelectableBean<UserType>, String> column = new PropertyColumn(
createStringResource("UserType.givenName"), UserType.F_GIVEN_NAME.getLocalPart(),
SelectableBean.F_VALUE + ".givenName");
columns.add(column);
column = new PropertyColumn(createStringResource("UserType.familyName"),
UserType.F_FAMILY_NAME.getLocalPart(), SelectableBean.F_VALUE + ".familyName");
columns.add(column);
column = new PropertyColumn(createStringResource("UserType.fullName"),
UserType.F_FULL_NAME.getLocalPart(), SelectableBean.F_VALUE + ".fullName");
columns.add(column);
column = new PropertyColumn(createStringResource("UserType.emailAddress"), null,
SelectableBean.F_VALUE + ".emailAddress");
columns.add(column);
column = new AbstractExportableColumn<SelectableBean<UserType>, String>(
createStringResource("pageUsers.accounts")) {
@Override
public void populateItem(Item<ICellPopulator<SelectableBean<UserType>>> cellItem,
String componentId, IModel<SelectableBean<UserType>> model) {
cellItem.add(new Label(componentId,
model.getObject().getValue() != null ?
model.getObject().getValue().getLinkRef().size() : null));
}
@Override
public IModel<String> getDataModel(IModel<SelectableBean<UserType>> rowModel) {
return Model.of(rowModel.getObject().getValue() != null ?
Integer.toString(rowModel.getObject().getValue().getLinkRef().size()) : "");
}
};
columns.add(column);
return columns;
}
private void userDetailsPerformed(AjaxRequestTarget target, String oid) {
PageParameters parameters = new PageParameters();
parameters.add(OnePageParameterEncoder.PARAMETER, oid);
navigateToNext(PageUser.class, parameters);
}
}
上述代码中table创建的语句是
//该语句首先定义了一个MainObjectListPanel的匿名继承类,然后创建这个匿名类
MainObjectListPanel<UserType> userListPanel = new MainObjectListPanel<UserType>(ID_TABLE,
UserType.class, TableId.TABLE_USERS, options, this){
}
MainObjectListPanel是 midpoint封装的一个类,本次分析table 数据获取有关的核心代码为
public abstract class MainObjectListPanel<O extends ObjectType> extends ObjectListPanel<O> {
public MainObjectListPanel(String id, Class<O> type, TableId tableId, Collection<SelectorOptions<GetOperationOptions>> options, PageBase parentPage) {
super(id, type, tableId, options, parentPage);
}
}
该类继承自ObjectListPanel,我们在看看这个类
//是table数据来源的主要实现类,从该类的createTable方法中,我们知道了,
// table数据的提供类是SelectableBeanObjectDataProvider
public abstract class ObjectListPanel<O extends ObjectType> extends BasePanel<O> {
private static final String ID_MAIN_FORM = "mainForm";
private static final String ID_TABLE = "table";
/**
* @param defaultType specifies type of the object that will be selected by default. It can be changed.
*/
public ObjectListPanel(String id, Class<? extends O> defaultType, TableId tableId, Collection<SelectorOptions<GetOperationOptions>> options,
PageBase parentPage) {
this(id, defaultType, tableId, options, false, parentPage, null);
}
/**
* @param defaultType specifies type of the object that will be selected by default. It can be changed.
*/
ObjectListPanel(String id, Class<? extends O> defaultType, TableId tableId, boolean multiselect, PageBase parentPage) {
this(id, defaultType, tableId, null, multiselect, parentPage, null);
}
public ObjectListPanel(String id, Class<? extends O> defaultType, TableId tableId, Collection<SelectorOptions<GetOperationOptions>> options,
boolean multiselect, PageBase parentPage, List<O> selectedObjectsList) {
super(id);
this.type = defaultType != null ? ObjectTypes.getObjectType(defaultType) : null;
this.parentPage = parentPage;
this.options = options;
this.multiselect = multiselect;
this.selectedObjects = selectedObjectsList;
this.tableId = tableId;
initLayout();
}
private void initLayout() {
Form<O> mainForm = new com.evolveum.midpoint.web.component.form.Form<>(ID_MAIN_FORM);
add(mainForm);
........
BoxedTablePanel<SelectableBean<O>> table = createTable();
mainForm.add(table);
}
private BoxedTablePanel<SelectableBean<O>> createTable() {
List<IColumn<SelectableBean<O>, String>> columns;
if (isCustomColumnsListConfigured()){
columns = initCustomColumns();
} else {
columns = initColumns();
}
BaseSortableDataProvider<SelectableBean<O>> provider = initProvider();
BoxedTablePanel<SelectableBean<O>> table = new BoxedTablePanel<SelectableBean<O>>(ID_TABLE, provider,
columns, tableId, tableId == null ? 10 : parentPage.getSessionStorage().getUserProfile().getPagingSize(tableId)) {
private static final long serialVersionUID = 1L;
@Override
protected WebMarkupContainer createHeader(String headerId) {
return ObjectListPanel.this.createHeader(headerId);
}
@Override
public String getAdditionalBoxCssClasses() {
return ObjectListPanel.this.getAdditionalBoxCssClasses();
}
@Override
protected WebMarkupContainer createButtonToolbar(String id) {
WebMarkupContainer bar = ObjectListPanel.this.createTableButtonToolbar(id);
return bar != null ? bar : super.createButtonToolbar(id);
}
};
table.setOutputMarkupId(true);
String storageKey = getStorageKey();
if (StringUtils.isNotEmpty(storageKey)) {
PageStorage storage = getPageStorage(storageKey);
if (storage != null) {
table.setCurrentPage(storage.getPaging());
}
}
return table;
}
protected List<IColumn<SelectableBean<O>, String>> initCustomColumns() {
LOGGER.trace("Start to init custom columns for table of type {}", type);
List<IColumn<SelectableBean<O>, String>> columns = new ArrayList<>();
List<GuiObjectColumnType> customColumns = getGuiObjectColumnTypeList();
if (customColumns == null){
return columns;
}
CheckBoxHeaderColumn<SelectableBean<O>> checkboxColumn = (CheckBoxHeaderColumn<SelectableBean<O>>) createCheckboxColumn();
if (checkboxColumn != null) {
columns.add(checkboxColumn);
}
IColumn<SelectableBean<O>, String> iconColumn = (IColumn) ColumnUtils.createIconColumn(type.getClassDefinition());
columns.add(iconColumn);
columns.addAll(getCustomColumnsTransformed(customColumns));
IColumn<SelectableBean<O>, String> actionsColumn = createActionsColumn();
if (actionsColumn != null){
columns.add(actionsColumn);
}
LOGGER.trace("Finished to init custom columns, created columns {}", columns);
return columns;
}
protected List<IColumn<SelectableBean<O>, String>> initColumns() {
LOGGER.trace("Start to init columns for table of type {}", type);
List<IColumn<SelectableBean<O>, String>> columns = new ArrayList<>();
CheckBoxHeaderColumn<SelectableBean<O>> checkboxColumn = (CheckBoxHeaderColumn<SelectableBean<O>>) createCheckboxColumn();
if (checkboxColumn != null) {
columns.add(checkboxColumn);
}
IColumn<SelectableBean<O>, String> iconColumn = (IColumn) ColumnUtils.createIconColumn(type.getClassDefinition());
columns.add(iconColumn);
IColumn<SelectableBean<O>, String> nameColumn = createNameColumn(null, null);
columns.add(nameColumn);
List<IColumn<SelectableBean<O>, String>> others = createColumns();
columns.addAll(others);
IColumn<SelectableBean<O>, String> actionsColumn = createActionsColumn();
if (actionsColumn != null) {
columns.add(createActionsColumn());
}
LOGGER.trace("Finished to init columns, created columns {}", columns);
return columns;
}
protected BaseSortableDataProvider<SelectableBean<O>> initProvider() {
Set<O> selectedObjectsSet = selectedObjects == null ? null : new HashSet<>(selectedObjects);
SelectableBeanObjectDataProvider<O> provider = new SelectableBeanObjectDataProvider<O>(
parentPage, (Class) type.getClassDefinition(), selectedObjectsSet) {
private static final long serialVersionUID = 1L;
@Override
protected void saveProviderPaging(ObjectQuery query, ObjectPaging paging) {
String storageKey = getStorageKey();
if (StringUtils.isNotEmpty(storageKey)) {
PageStorage storage = getPageStorage(storageKey);
if (storage != null) {
storage.setPaging(paging);
}
}
}
@Override
public SelectableBean<O> createDataObjectWrapper(O obj) {
SelectableBean<O> bean = super.createDataObjectWrapper(obj);
List<InlineMenuItem> inlineMenu = createInlineMenu();
if (inlineMenu != null) {
bean.getMenuItems().addAll(inlineMenu);
}
return bean;
}
@Override
protected List<ObjectOrdering> createObjectOrderings(SortParam<String> sortParam) {
List<ObjectOrdering> customOrdering = createCustomOrdering(sortParam);
if (customOrdering != null) {
return customOrdering;
}
return super.createObjectOrderings(sortParam);
}
};
if (options == null){
if (ResourceType.class.equals(type)) {
options = SelectorOptions.createCollection(GetOperationOptions.createNoFetch());
}
} else {
if (ResourceType.class.equals(type)) {
GetOperationOptions root = SelectorOptions.findRootOptions(options);
root.setNoFetch(Boolean.TRUE);
}
provider.setOptions(options);
}
provider.setQuery(getQuery());
return provider;
}
protected abstract IColumn<SelectableBean<O>, String> createCheckboxColumn();
protected abstract IColumn<SelectableBean<O>, String> createNameColumn(IModel<String> columnNameModel, String itemPath);
protected abstract List<IColumn<SelectableBean<O>, String>> createColumns();
protected IColumn<SelectableBean<O>, String> createActionsColumn(){
return null;
}
protected abstract List<InlineMenuItem> createInlineMenu();
........
}
我们看一下SelectableBeanObjectDataProvider类
//从该类中我们看到该Provider类中,并未有wicket默认Provider应该实现的iterator方法,
//继续查看父类,我们可在BaseSortableDataProvider类中找到该方法,在此就不再列代码了
//仅做一个说明,BaseSortableDataProvider类的iterator方法,调用的是internalIterator方法
//我们详细看看这个方法发现,获取数据的核心语句是
//List<PrismObject<? extends O>> list = (List)getModel().searchObjects(type, query, currentOptions, task, result);
//该语句中的getModel()返回的是怎样一个类呢,单步调试,我们发现该类是
//com.evolveum.midpoint.model.impl.controller.ModelController
public class SelectableBeanObjectDataProvider<O extends ObjectType> extends BaseSortableDataProvider<SelectableBean<O>> {
public SelectableBeanObjectDataProvider(Component component, Class<? extends O> type, Set<? extends O> selected ) {
super(component, true, true);
}
@Override
public Iterator<SelectableBean<O>> internalIterator(long offset, long pageSize) {
LOGGER.trace("begin::iterator() offset {} pageSize {}.", new Object[]{offset, pageSize});
preprocessSelectedData();
OperationResult result = new OperationResult(OPERATION_SEARCH_OBJECTS);
try {
ObjectPaging paging = createPaging(offset, pageSize);
Task task = getPage().createSimpleTask(OPERATION_SEARCH_OBJECTS);
ObjectQuery query = getQuery();
if (query == null){
if (emptyListOnNullQuery) {
return new ArrayList<SelectableBean<O>>().iterator();
}
query = new ObjectQuery();
}
query.setPaging(paging);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Query {} with {}", type.getSimpleName(), query.debugDump());
}
if (ResourceType.class.equals(type) && (options == null || options.isEmpty())){
options = SelectorOptions.createCollection(GetOperationOptions.createNoFetch());
}
Collection<SelectorOptions<GetOperationOptions>> currentOptions = options;
if (export) {
// TODO also for other classes
if (ShadowType.class.equals(type)) {
currentOptions = SelectorOptions.set(currentOptions, ItemPath.EMPTY_PATH, () -> new GetOperationOptions(),
(o) -> o.setDefinitionProcessing(ONLY_IF_EXISTS));
currentOptions = SelectorOptions
.set(currentOptions, new ItemPath(ShadowType.F_FETCH_RESULT), GetOperationOptions::new,
(o) -> o.setDefinitionProcessing(FULL));
currentOptions = SelectorOptions
.set(currentOptions, new ItemPath(ShadowType.F_AUXILIARY_OBJECT_CLASS), GetOperationOptions::new,
(o) -> o.setDefinitionProcessing(FULL));
}
}
List<PrismObject<? extends O>> list = (List)getModel().searchObjects(type, query, currentOptions, task, result);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Query {} resulted in {} objects", type.getSimpleName(), list.size());
}
for (PrismObject<? extends O> object : list) {
getAvailableData().add(createDataObjectWrapper(object.asObjectable()));
}
// result.recordSuccess();
} catch (Exception ex) {
result.recordFatalError("Couldn't list objects.", ex);
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't list objects", ex);
return handleNotSuccessOrHandledErrorInIterator(result);
} finally {
result.computeStatusIfUnknown();
}
LOGGER.trace("end::iterator() {}", result);
return getAvailableData().iterator();
}
@Override
protected int internalSize() {
LOGGER.trace("begin::internalSize()");
if (!isUseObjectCounting()) {
return Integer.MAX_VALUE;
}
int count = 0;
Task task = getPage().createSimpleTask(OPERATION_COUNT_OBJECTS);
OperationResult result = task.getResult();
try {
Integer counted = getModel().countObjects(type, getQuery(), options, task, result);
count = defaultIfNull(counted, 0);
} catch (Exception ex) {
result.recordFatalError("Couldn't count objects.", ex);
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't count objects", ex);
} finally {
result.computeStatusIfUnknown();
}
if (!WebComponentUtil.isSuccessOrHandledError(result) && !result.isNotApplicable()) {
getPage().showResult(result);
// Let us do nothing. The error will be shown on the page and a count of 0 will be used.
// Redirecting to the error page does more harm than good (see also MID-4306).
}
LOGGER.trace("end::internalSize(): {}", count);
return count;
}
}
我们来看一下com.evolveum.midpoint.model.impl.controller.ModelController中的searchObject方法。
//通过看该方法,我们知道获取数据的语句是
//case REPOSITORY: list = cacheRepositoryService.searchObjects(type, query, options, result); break;
public class ModelController implements ModelService, TaskService, WorkflowService, ScriptingService, AccessCertificationService, CaseManagementService {
@Override
public <T extends ObjectType> SearchResultList<PrismObject<T>> searchObjects(Class<T> type, ObjectQuery query,
Collection<SelectorOptions<GetOperationOptions>> rawOptions, Task task, OperationResult parentResult) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException, SecurityViolationException, ExpressionEvaluationException {
Validate.notNull(type, "Object type must not be null.");
Validate.notNull(parentResult, "Operation result must not be null.");
if (query != null) {
ModelImplUtils.validatePaging(query.getPaging());
}
OperationResult result = parentResult.createSubresult(SEARCH_OBJECTS);
result.addParam(OperationResult.PARAM_TYPE, type);
result.addParam(OperationResult.PARAM_QUERY, query);
Collection<SelectorOptions<GetOperationOptions>> options = preProcessOptionsSecurity(rawOptions, task, result);
GetOperationOptions rootOptions = SelectorOptions.findRootOptions(options);
ObjectTypes.ObjectManager searchProvider = ObjectTypes.getObjectManagerForClass(type);
if (searchProvider == null || searchProvider == ObjectTypes.ObjectManager.MODEL || GetOperationOptions.isRaw(rootOptions)) {
searchProvider = ObjectTypes.ObjectManager.REPOSITORY;
}
result.addArbitraryObjectAsParam("searchProvider", searchProvider);
query = preProcessQuerySecurity(type, query, task, result);
if (isFilterNone(query, result)) {
return new SearchResultList<>(new ArrayList<>());
}
SearchResultList<PrismObject<T>> list;
try {
enterModelMethod();
logQuery(query);
try {
if (GetOperationOptions.isRaw(rootOptions)) { // MID-2218
QNameUtil.setTemporarilyTolerateUndeclaredPrefixes(true);
}
switch (searchProvider) {
case EMULATED: list = emulatedSearchProvider.searchObjects(type, query, options, result); break;
case REPOSITORY: list = cacheRepositoryService.searchObjects(type, query, options, result); break;
case PROVISIONING: list = provisioning.searchObjects(type, query, options, task, result); break;
case TASK_MANAGER:
list = taskManager.searchObjects(type, query, options, result);
if (workflowManager != null && TaskType.class.isAssignableFrom(type) && !GetOperationOptions.isRaw(rootOptions) && !GetOperationOptions.isNoFetch(rootOptions)) {
workflowManager.augmentTaskObjectList(list, options, task, result);
}
break;
default: throw new AssertionError("Unexpected search provider: " + searchProvider);
}
result.computeStatus();
result.cleanupResult();
} catch (CommunicationException | ConfigurationException | SchemaException | SecurityViolationException | RuntimeException | ObjectNotFoundException e) {
processSearchException(e, rootOptions, searchProvider, result);
throw e;
} finally {
QNameUtil.setTemporarilyTolerateUndeclaredPrefixes(false);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(result.dump(false));
}
}
if (list == null) {
list = new SearchResultList<>(new ArrayList<PrismObject<T>>());
}
for (PrismObject<T> object : list) {
if (hookRegistry != null) {
for (ReadHook hook : hookRegistry.getAllReadHooks()) {
hook.invoke(object, options, task, result);
}
}
executeResolveOptions(object.asObjectable(), options, task, result);
}
// postprocessing objects that weren't handled by their correct provider (e.g. searching for ObjectType, and retrieving tasks, resources, shadows)
// currently only resources and shadows are handled in this way
// TODO generalize this approach somehow (something like "postprocess" in task/provisioning interface)
if (searchProvider == ObjectTypes.ObjectManager.REPOSITORY && !GetOperationOptions.isRaw(rootOptions)) {
for (PrismObject<T> object : list) {
if (object.asObjectable() instanceof ResourceType || object.asObjectable() instanceof ShadowType) {
provisioning.applyDefinition(object, task, result);
}
}
}
// better to use cache here (MID-4059)
schemaTransformer.applySchemasAndSecurityToObjects(list, rootOptions, options, null, task, result);
} finally {
exitModelMethod();
}
return list;
}
@Override
public <T extends Containerable> Integer countContainers(
Class<T> type, ObjectQuery query, Collection<SelectorOptions<GetOperationOptions>> rawOptions,
Task task, OperationResult parentResult) throws SchemaException, SecurityViolationException, ObjectNotFoundException, ExpressionEvaluationException, CommunicationException, ConfigurationException {
Validate.notNull(type, "Container value type must not be null.");
Validate.notNull(parentResult, "Result type must not be null.");
final OperationResult result = parentResult.createSubresult(SEARCH_CONTAINERS);
result.addParam(OperationResult.PARAM_TYPE, type);
result.addParam(OperationResult.PARAM_QUERY, query);
final ContainerOperationContext<T> ctx = new ContainerOperationContext<>(type, query, task, result);
final Collection<SelectorOptions<GetOperationOptions>> options = preProcessOptionsSecurity(rawOptions, task, result);
final GetOperationOptions rootOptions = SelectorOptions.findRootOptions(options);
query = ctx.refinedQuery;
if (isFilterNone(query, result)) {
return 0;
}
Integer count;
try {
enterModelMethod();
logQuery(query);
try {
switch (ctx.manager) {
case REPOSITORY: count = cacheRepositoryService.countContainers(type, query, options, result); break;
case WORKFLOW: count = workflowManager.countContainers(type, query, options, result); break;
default: throw new IllegalStateException();
}
result.computeStatus();
result.cleanupResult();
} catch (SchemaException|RuntimeException e) {
processSearchException(e, rootOptions, ctx.manager, result);
throw e;
} finally {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(result.dump(false));
}
}
} finally {
exitModelMethod();
}
return count;
}
}
cacheRepositoryService通过查看源码可知,其对于的类为com.evolveum.midpoint.repo.cache.RepositoryCache
@Component(value="cacheRepositoryService")
public class RepositoryCache implements RepositoryService {}
该分析就分析到这块儿,其后就是midpoint底层存储库的核心代码了,在此不深入描述,本篇主要介绍如何使用midpoint提供的存储库接口 实现table的展示。另外,上面代码列创建的核心代码已列出,table中的列如何创建,请自行看代码。
下对刚才的代码做个简短的总结,midpoint源码创建table的方式是
1、BoxedTablePanel<SelectableBean<O>> table = new BoxedTablePanel<SelectableBean<O>>(ID_TABLE, provider,
columns, tableId, pagesize)
2、实现provider的iterator方法
3、cacheRepositoryService.searchObjects(type, query, options, result);