Java tutorial
///////////////////////////////////////////////////////////////////////////// // // Project ProjectForge Community Edition // www.projectforge.org // // Copyright (C) 2001-2013 Kai Reinhard (k.reinhard@micromata.de) // // ProjectForge is dual-licensed. // // This community edition is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 3 of the License. // // This community edition is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General // Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, see http://www.gnu.org/licenses/. // ///////////////////////////////////////////////////////////////////////////// package org.projectforge.web.wicket; import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.apache.wicket.AttributeModifier; import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder; import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable; import org.apache.wicket.extensions.markup.html.repeater.data.table.DefaultDataTable; import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn; import org.apache.wicket.extensions.markup.html.repeater.data.table.ISortableDataProvider; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.markup.html.link.Link; import org.apache.wicket.markup.html.panel.EmptyPanel; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.model.Model; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.projectforge.common.RecentQueue; import org.projectforge.common.ReflectionHelper; import org.projectforge.common.StringHelper; import org.projectforge.core.BaseDO; import org.projectforge.core.BaseDao; import org.projectforge.core.BaseSearchFilter; import org.projectforge.core.IdObject; import org.projectforge.core.UserException; import org.projectforge.web.fibu.ISelectCallerPage; import org.projectforge.web.wicket.components.ContentMenuEntryPanel; import org.projectforge.web.wicket.flowlayout.IconType; public abstract class AbstractListPage<F extends AbstractListForm<?, ?>, D extends org.projectforge.core.IDao<?>, O extends IdObject<?>> extends AbstractSecuredPage implements ISelectCallerPage { private static final long serialVersionUID = 622509418161777195L; private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(AbstractListPage.class); public static final String PARAMETER_KEY_STORE_FILTER = "storeFilter"; public static final String PARAMETER_KEY_FILTER = "f"; public static final String PARAMETER_KEY_SEARCH_STRING = PARAMETER_KEY_FILTER + ".s"; public static final String PARAMETER_HIGHLIGHTED_ROW = "row"; private boolean calledBySearchPage; private List<O> resultList; private boolean refreshResultList = true; /** * For selecting items for mass update (only used by some pages). */ protected Set<Integer> selectedItems; protected static final String[] BOOKMARKABLE_INITIAL_PROPERTIES = new String[] { "f.searchString|s", "f.useModificationFilter|mod", "f.modifiedByUserId|mUser", "f.startTimeOfLastModification|mStart", "f.stopTimeOfLastModification|mStop", "f.deleted|del", "pageSize" }; protected static final String[] mergeStringArrays(final String[] a1, final String a2[]) { final String[] result = new String[a1.length + a2.length]; int pos = 0; for (final String str : a1) { result[pos++] = str; } for (final String str : a2) { result[pos++] = str; } return result; } protected F form; protected DataTable<O, String> dataTable; private Serializable highlightedRowId; protected ISelectCallerPage caller; protected String selectProperty; protected String i18nPrefix; protected ContentMenuEntryPanel newItemMenuEntry; protected ContentMenuEntryPanel massUpdateMenuEntry; protected ContentMenuEntryPanel selectAllMenuEntry; protected ContentMenuEntryPanel deselectAllMenuEntry; protected boolean storeFilter = true; private boolean massUpdateMode = false; protected MyListPageSortableDataProvider<O> listPageSortableDataProvider; /** * Change this value if the recent search terms should be stored. Should be set in setup-method of derived page class. */ protected String recentSearchTermsUserPrefKey = null; protected RecentQueue<String> recentSearchTermsQueue; public static void addRowClick(final Item<?> cellItem) { final Item<?> row = (cellItem.findParent(Item.class)); WicketUtils.addRowClick(row); } /** * @param cellItem * @param massUpdate If true then a mouse click on the row should (de)activate the check box to select the row for the mass update, * otherwise this method calls addRowClick(Item). * @see #addRowClick(Item) */ protected static void addRowClick(final Item<?> cellItem, final boolean massUpdate) { if (massUpdate == true) { final Item<?> row = (cellItem.findParent(Item.class)); row.add(AttributeModifier.replace("onmousedown", "javascript:rowCheckboxClick(this, event);")); } else { addRowClick(cellItem); } } protected AbstractListPage(final PageParameters parameters, final String i18nPrefix) { this(parameters, null, null, i18nPrefix); } protected AbstractListPage(final ISelectCallerPage caller, final String selectProperty, final String i18nPrefix) { this(new PageParameters(), caller, selectProperty, i18nPrefix); } protected AbstractListPage(final PageParameters parameters, final ISelectCallerPage caller, final String selectProperty, final String i18nPrefix) { super(parameters); if (parameters.get(PARAMETER_KEY_STORE_FILTER) != null) { final Boolean flag = WicketUtils.getAsBooleanObject(parameters, PARAMETER_KEY_STORE_FILTER); if (flag != null && flag == false) { storeFilter = false; } } if (parameters.get(PARAMETER_HIGHLIGHTED_ROW) != null) { setHighlightedRowId(WicketUtils.getAsInteger(parameters, PARAMETER_HIGHLIGHTED_ROW)); } this.i18nPrefix = i18nPrefix; this.caller = caller; this.selectProperty = selectProperty; setup(); preInit(); evaluateInitialPageParameters(parameters); } /** * Copies all fields of the given filter to the current filter of the form. * @param filter */ public void copySearchFieldsFrom(final BaseSearchFilter filter) { form.copySearchFieldsFrom(filter); } /** * Is called before the form is initialized in constructor. Overwrite this method if any variables etc. should be set before * initialization. */ protected void setup() { } /** * Highlight the row representing the data object with the given id. * @param highlightedRowId */ public void setHighlightedRowId(final Serializable highlightedRowId) { this.highlightedRowId = highlightedRowId; } public Serializable getHighlightedRowId() { return highlightedRowId; } private F getForm() { if (form == null) { form = newListForm(this); } return form; } /** * @param item The item where to add the css classes. * @param rowDataId If the current row data is equals to the hightlightedRow then the style will contain highlighting. * @param highlightedRowId The current row to highlight (id of the data object behind the row). * @param isDeleted Is this entry deleted? Then the deleted style will be added. * @return */ protected static void appendCssClasses(final Item<?> item, final Serializable rowDataId, final Serializable highlightedRowId, final boolean isDeleted) { if (rowDataId == null) { return; } if (rowDataId instanceof Integer == false) { log.warn("Error in calling getCssStyle: Integer expected instead of " + rowDataId.getClass()); } if (highlightedRowId != null && rowDataId != null && ObjectUtils.equals(highlightedRowId, rowDataId) == true) { appendCssClasses(item, RowCssClass.HIGHLIGHTED); } if (isDeleted == true) { appendCssClasses(item, RowCssClass.MARKED_AS_DELETED); } } /** * Adds some standard css classes such as {@link RowCssClass#MARKED_AS_DELETED} for deleted entries. * @param item The item where to add the css classes. * @param rowDataId If the current row data is equals to the hightlightedRow then the style will contain highlighting. * @param isDeleted Is this entry deleted? Then the deleted style will be added. * @return */ protected void appendCssClasses(final Item<?> item, final Serializable rowDataId, final boolean isDeleted) { appendCssClasses(item, rowDataId, this.highlightedRowId, isDeleted); } /** * @param item The item where to add the css classes. * @param rowCssClasses The css class to append to the given item. * @return */ protected static void appendCssClasses(final Item<?> item, final RowCssClass... rowCssClasses) { WicketUtils.append(item, rowCssClasses); } /** * Adds storeFilter=false to the parameters. * @see org.projectforge.web.wicket.AbstractSecuredPage#getBookmarkableInitialParameters() */ @Override public PageParameters getBookmarkableInitialParameters() { final PageParameters pageParameters = super.getBookmarkableInitialParameters(); WicketUtils.addOrReplaceParameter(pageParameters, PARAMETER_KEY_STORE_FILTER, false); return pageParameters; } /** * @see org.projectforge.web.wicket.AbstractSecuredPage#getBookmarkableInitialProperties() */ @Override protected String[] getBookmarkableInitialProperties() { return BOOKMARKABLE_INITIAL_PROPERTIES; } /** * @see org.projectforge.web.wicket.AbstractSecuredPage#getFilterObjectForInitialParameters() */ @Override protected Object getFilterObjectForInitialParameters() { return form.getSearchFilter(); } /** * @return the form. * @see org.projectforge.web.wicket.AbstractSecuredPage#getDataObjectForInitialParameters() */ @Override protected Object getDataObjectForInitialParameters() { return form; } /** * @return This page as link with the page parameters of this page. */ @Override public String getPageAsLink() { return getPageAsLink(new PageParameters()); } @SuppressWarnings("serial") private void preInit() { getForm(); body.add(form); form.init(); if (isSelectMode() == false && (accessChecker.isDemoUser() == true || getBaseDao().hasInsertAccess(getUser()) == true)) { newItemMenuEntry = new ContentMenuEntryPanel(contentMenuBarPanel.newChildId(), new Link<Object>("link") { @Override public void onClick() { redirectToEditPage(null); }; }, IconType.PLUS); newItemMenuEntry.setAccessKey(WebConstants.ACCESS_KEY_ADD).setTooltip( getString(WebConstants.ACCESS_KEY_ADD_TOOLTIP_TITLE), getString(WebConstants.ACCESS_KEY_ADD_TOOLTIP)); contentMenuBarPanel.addMenuEntry(newItemMenuEntry); } final Label hintQuickSelectLabel = new Label("hintQuickSelect", new Model<String>(getString("hint.selectMode.quickselect"))) { @Override public boolean isVisible() { return isSelectMode(); } }; if (isSupportsMassUpdate() == true) { massUpdateMenuEntry = new ContentMenuEntryPanel(contentMenuBarPanel.newChildId(), new Link<Object>("link") { @Override public void onClick() { setMassUpdateMode(true); }; }, getString("massUpdate")); contentMenuBarPanel.addMenuEntry(massUpdateMenuEntry); ExternalLink link = new ExternalLink("link", "#"); link.add(AttributeModifier.replace("onclick", "javascript:selectAll();")); selectAllMenuEntry = new ContentMenuEntryPanel(contentMenuBarPanel.newChildId(), link, getString("selectAll")); selectAllMenuEntry.setVisible(false); contentMenuBarPanel.addMenuEntry(selectAllMenuEntry); link = new ExternalLink("link", "#"); link.add(AttributeModifier.replace("onclick", "javascript:deselectAll();")); deselectAllMenuEntry = new ContentMenuEntryPanel(contentMenuBarPanel.newChildId(), link, getString("deselectAll")); deselectAllMenuEntry.setVisible(false); contentMenuBarPanel.addMenuEntry(deselectAllMenuEntry); } form.add(hintQuickSelectLabel); addTopRightMenu(); addTopPanel(); addBottomPanel("bottomPanel"); init(); createDataTable(); } /** * Will be called by the constructors. */ protected abstract void init(); /** * For list pages which supports mass update, please implement this method. */ protected void createDataTable() { } /** * Called if the user clicks on the "new" (new entry) link. * @param params nullable or set by derived class methods before calling super.onNewClick(); * @return The edit page (response page). The return value has no effect. It's only useful for derived class methods which calls * super.onNewClick(); */ protected AbstractEditPage<?, ?, ?> redirectToEditPage(PageParameters params) { if (params == null) { params = new PageParameters(); } final Class<?> editPageClass = getClass().getAnnotation(ListPage.class).editPage(); final AbstractEditPage<?, ?, ?> editPage = (AbstractEditPage<?, ?, ?>) ReflectionHelper .newInstance(editPageClass, PageParameters.class, params); editPage.setReturnToPage(AbstractListPage.this); setResponsePage(editPage); return editPage; } protected abstract D getBaseDao(); /** * @return true, if response page is set for redirect (e. g. for successful quick selection), otherwise false. */ @SuppressWarnings("unchecked") protected boolean onSearchSubmit() { log.debug("onSearchSubmit"); refresh(); if (isSelectMode() == true) { final List<O> list = getList(); if (list != null && list.size() == 1) { // Quick select: final O obj = list.get(0); caller.select(selectProperty, ((BaseDO<Integer>) obj).getId()); WicketUtils.setResponsePage(this, caller); return true; } } else { // auto-select of a single entry: // final String searchString = form.searchFilter.getSearchString(); // if (searchString != null && searchString.matches("id:[0-9]+") == true) { // final Integer id = NumberHelper.parseInteger(searchString.substring(3)); // if (id != null) { // final PageParameters pageParams = new PageParameters(); // pageParams.add(AbstractEditPage.PARAMETER_KEY_ID, String.valueOf(id)); // redirectToEditPage(pageParams); // return true; // } // } } return false; } protected void onResetSubmit() { log.debug("onResetSubmit"); form.getSearchFilter().reset(); refresh(); form.clearInput(); } /** * User has pressed the cancel button. If in selection mode then redirect to the caller. */ protected void onCancelSubmit() { log.debug("onCancelSubmit"); if (isSelectMode() == true && caller != null) { WicketUtils.setResponsePage(this, caller); caller.cancelSelection(selectProperty); } else if (isMassUpdateMode() == true) { setMassUpdateMode(false); } } public void setMassUpdateMode(final boolean mode) { massUpdateMenuEntry.setVisible(!mode); selectAllMenuEntry.setVisible(mode); deselectAllMenuEntry.setVisible(mode); newItemMenuEntry.setVisible(!mode); this.massUpdateMode = mode; form.remove(dataTable); createDataTable(); form.setComponentsVisibility(); if (mode == true && selectedItems == null) { selectedItems = new HashSet<Integer>(); } } protected void onNextSubmit() { setResponsePage(new MessagePage("message.notYetImplemented")); } /** * Called, if the list must be refreshed. Sets list to null and page size of data table. */ public void refresh() { this.resultList = null; // Force reload of list this.refreshResultList = true; final long itemsPerPage = dataTable.getItemsPerPage(); if (form.getPageSize() != null && form.getPageSize().longValue() != itemsPerPage) { dataTable.setItemsPerPage(form.getPageSize()); } addRecentSearchTerm(); } public final List<O> getList() { if (this.refreshResultList == false && this.resultList != null) { return this.resultList; } this.refreshResultList = false; try { this.resultList = buildList(); listPageSortableDataProvider.setCompleteList(this.resultList); if (this.resultList == null) { // An error occured: form.addError("search.error"); } return this.resultList; } catch (final Exception ex) { if (ex instanceof UserException) { final UserException userException = (UserException) ex; error(getLocalizedMessage(userException.getI18nKey(), userException.getParams())); } else { log.error(ex.getMessage(), ex); } } return this.resultList = new ArrayList<O>(); } @SuppressWarnings("unchecked") protected List<O> buildList() { return (List<O>) getBaseDao().getList(form.getSearchFilter()); } /** * @see org.projectforge.web.wicket.AbstractUnsecureBasePage#onBeforeRender() */ @Override protected void onBeforeRender() { if (this.refreshResultList == true) { getList(); } super.onBeforeRender(); } /** * @see org.apache.wicket.markup.html.WebPage#onAfterRender() */ @Override protected void onAfterRender() { super.onAfterRender(); this.resultList = null; // Don't waste memory. } protected abstract F newListForm(AbstractListPage<?, ?, ?> parentPage); protected String getSearchToolTip() { return getLocalizedMessage("search.string.info", getSearchFields()); } @SuppressWarnings("serial") protected void addTopRightMenu() { if (isSelectMode() == false && ((getBaseDao() instanceof BaseDao<?>) || providesOwnRebuildDatabaseIndex() == true)) { new AbstractReindexTopRightMenu(this.contentMenuBarPanel, accessChecker.isLoggedInUserMemberOfAdminGroup()) { @Override protected void rebuildDatabaseIndex(final boolean onlyNewest) { if (providesOwnRebuildDatabaseIndex() == true) { ownRebuildDatabaseIndex(onlyNewest); } else { if (onlyNewest == true) { ((BaseDao<?>) getBaseDao()).rebuildDatabaseIndex4NewestEntries(); } else { ((BaseDao<?>) getBaseDao()).rebuildDatabaseIndex(); } } } @Override protected String getString(final String i18nKey) { return AbstractListPage.this.getString(i18nKey); } }; } } protected boolean providesOwnRebuildDatabaseIndex() { return false; } protected void ownRebuildDatabaseIndex(final boolean onlyNewest) { } /** * Override this method if you need a top panel. The default top panel is empty and not visible. */ protected void addTopPanel() { final Panel topPanel = new EmptyPanel("topPanel"); topPanel.setVisible(false); form.add(topPanel); } /** * Override this method if you need a bottom panel. The default bottom panel is empty and not visible. */ protected void addBottomPanel(final String id) { final Panel bottomPanel = new EmptyPanel(id); bottomPanel.setVisible(false); form.add(bottomPanel); } public boolean isMassUpdateMode() { return massUpdateMode; } /** * Overwrite this method if your list page does support mass update. * @return false at default. */ public boolean isSupportsMassUpdate() { return false; } /** * Later: Try AjaxFallBackDatatable again. * @param columns * @param sortProperty * @param ascending * @return */ protected DataTable<O, String> createDataTable(final List<IColumn<O, String>> columns, final String sortProperty, final SortOrder sortOrder) { final int pageSize = form.getPageSize(); return new DefaultDataTable<O, String>("table", columns, createSortableDataProvider(sortProperty, sortOrder), pageSize); // return new AjaxFallbackDefaultDataTable<O>("table", columns, createSortableDataProvider(sortProperty, ascending), pageSize); } /** * At default a new SortableDOProvider is returned. Overload this method e. g. for avoiding LazyInitializationExceptions due to sorting. * @param sortProperty * @param ascending */ protected ISortableDataProvider<O, String> createSortableDataProvider(final String sortProperty, final SortOrder sortOrder) { if (listPageSortableDataProvider == null) { listPageSortableDataProvider = new MyListPageSortableDataProvider<O>(sortProperty, sortOrder, this); } return listPageSortableDataProvider; } /** * For displaying the hibernate search fields. Returns list as csv. These fields the user can directly address in his search string, e. g. * street:marie. * @return * @see org.projectforge.core.BaseDao#getSearchFields() */ public String getSearchFields() { return StringHelper.listToString(", ", getBaseDao().getSearchFields()); } /** * @return true, if this page is called for selection by a caller otherwise false. */ public boolean isSelectMode() { return this.caller != null; } /** * Calls getString(key) with key "[i18nPrefix].title.list" or "[i18nPrefix].title.list.select" dependent weather the list is shown for * browsing or selecting (select mode). * @see org.projectforge.web.wicket.AbstractUnsecureBasePage#getTitle() * @see #isSelectMode() */ @Override protected String getTitle() { if (isSelectMode() == true) { return getString(i18nPrefix + ".title.list.select"); } else { return getString(i18nPrefix + ".title.list"); } } /** * If false then the action filter will not be stored (the previous stored filter will be preserved). true is default. */ public boolean isStoreFilter() { return storeFilter; } /** * Does nothing at default. If overload, don't forget to call super.cancelSelection(String) if no property matches. * @see org.projectforge.web.fibu.ISelectCallerPage#cancelSelection(java.lang.String) */ public void cancelSelection(final String property) { // Do nothing. } /** * Handles modifiedByUserId. If overload, don't forget to call super.select(String) if no property matches. * @see org.projectforge.web.wicket.AbstractListPage#select(java.lang.String, java.lang.Object) */ public void select(final String property, final Object selectedValue) { if ("modifiedByUserId".equals(property) == true) { form.getSearchFilter().setModifiedByUserId((Integer) selectedValue); form.getSearchFilter().setUseModificationFilter(true); refresh(); } else { log.error("Property '" + property + "' not supported for selection in class " + getClass().getName() + "."); } } /** * Handles modifiedByUserId. If overload, don't forget to call super.select(String) if no property matches. * @see org.projectforge.web.fibu.ISelectCallerPage#unselect(java.lang.String) */ public void unselect(final String property) { if ("modifiedByUserId".equals(property) == true) { form.getSearchFilter().setModifiedByUserId(null); form.getSearchFilter().setUseModificationFilter(true); refresh(); } else { log.error("Property '" + property + "' not supported for selection in class " + getClass().getName() + "."); } } @SuppressWarnings("unchecked") public RecentQueue<String> getRecentSearchTermsQueue() { if (recentSearchTermsQueue == null) { recentSearchTermsQueue = (RecentQueue<String>) getUserPrefEntry(this.recentSearchTermsUserPrefKey); } if (recentSearchTermsQueue == null) { recentSearchTermsQueue = new RecentQueue<String>(); if (isRecentSearchTermsStorage() == true) { putUserPrefEntry(this.recentSearchTermsUserPrefKey, recentSearchTermsQueue, true); } } return recentSearchTermsQueue; } /** * Adds the search string to the recent list, if filter is from type BaseSearchFilter and the search string is not blank and not from type * id:4711. * @param Filter The search filter. */ protected void addRecentSearchTerm() { if (StringUtils.isNotBlank(form.searchFilter.getSearchString()) == true) { final String s = form.searchFilter.getSearchString(); if (s.startsWith("id:") == false || StringUtils.isNumeric(s.substring(3)) == false) { // OK, search string is not from type id:4711 getRecentSearchTermsQueue().append(s); } } } /** * @return True, if the user-pref-key for storing the recent search terms is given, otherwise false. */ public boolean isRecentSearchTermsStorage() { return this.recentSearchTermsUserPrefKey != null; } /** * Tiny helper method. * @param propertyName * @param sortable * @return return sortable ? propertyName : null; */ protected static String getSortable(final String propertyName, final boolean sortable) { return sortable ? propertyName : null; } /** * ONLY for internal purposes to tell the IListPageColumnsCreator that it's instantiated by the SearchAreaPanel. * @param calledBySearchPage the calledBySearchForm to set */ public void setCalledBySearchPage(final boolean calledBySearchPage) { this.calledBySearchPage = calledBySearchPage; } /** * @return the calledBySearchForm */ public boolean isCalledBySearchPage() { return calledBySearchPage; } @SuppressWarnings("serial") public class SelectItemModel extends Model<Boolean> { Integer id; public SelectItemModel(final Integer id) { this.id = id; } @Override public Boolean getObject() { return selectedItems.contains(id); } @Override public void setObject(final Boolean object) { if (Boolean.TRUE.equals(object) == true) { selectedItems.add(id); } else { selectedItems.remove(id); } } } }