/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 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.text.NumberFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.HiddenField;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.projectforge.common.DateHolder;
import org.projectforge.common.DatePrecision;
import org.projectforge.common.NumberHelper;
import org.projectforge.core.BaseDao;
import org.projectforge.core.BaseSearchFilter;
import org.projectforge.user.PFUserContext;
import org.projectforge.user.PFUserDO;
import org.projectforge.user.UserGroupCache;
import org.projectforge.web.user.UserSelectPanel;
import org.projectforge.web.wicket.bootstrap.GridBuilder;
import org.projectforge.web.wicket.bootstrap.GridSize;
import org.projectforge.web.wicket.components.DateTimePanel;
import org.projectforge.web.wicket.components.DateTimePanelSettings;
import org.projectforge.web.wicket.components.LabelValueChoiceRenderer;
import org.projectforge.web.wicket.components.SingleButtonPanel;
import org.projectforge.web.wicket.flowlayout.CheckBoxButton;
import org.projectforge.web.wicket.flowlayout.DivPanel;
import org.projectforge.web.wicket.flowlayout.FieldSetIconPosition;
import org.projectforge.web.wicket.flowlayout.FieldsetPanel;
import org.projectforge.web.wicket.flowlayout.HiddenInputPanel;
import org.projectforge.web.wicket.flowlayout.HtmlCommentPanel;
import org.projectforge.web.wicket.flowlayout.IconPanel;
import org.projectforge.web.wicket.flowlayout.IconType;
import org.projectforge.web.wicket.flowlayout.InputPanel;
import org.projectforge.web.wicket.flowlayout.MyComponentsRepeater;
public abstract class AbstractListForm<F extends BaseSearchFilter, P extends AbstractListPage< ? , ? , ? >> extends
AbstractSecuredForm<F, P>
{
private static final long serialVersionUID = 1304394324524767035L;
public static final String I18N_ONLY_DELETED = "onlyDeleted";
public static final String I18N_ONLY_DELETED_TOOLTIP = "onlyDeleted.tooltip";
protected F searchFilter;
protected abstract F newSearchFilterInstance();
protected Integer pageSize;
protected GridBuilder gridBuilder;
private DivPanel extendedFilter;
private Label modifiedSearchExpressionLabel;
private String modificationSince;
protected DateTimePanel startDateTimePanel;
protected DateTimePanel stopDateTimePanel;
@SpringBean(name = "userGroupCache")
protected UserGroupCache userGroupCache;
private FieldsetPanel optionsFieldsetPanel;
/**
* List to create action buttons in the desired order before creating the RepeatingView.
*/
protected MyComponentsRepeater<Component> actionButtons;
private SingleButtonPanel cancelButtonPanel;
protected SingleButtonPanel resetButtonPanel;
protected SingleButtonPanel searchButtonPanel;
private SingleButtonPanel nextButtonPanel;
protected FieldsetPanel pageSizeFieldsetPanel;
public static final int[] PAGE_SIZES = new int[] { 3, 5, 10, 25, 50, 100, 200, 500, 1000};
public static DropDownChoice<Integer> getPageSizeDropDownChoice(final String id, final Locale locale, final IModel<Integer> model,
final int minValue, final int maxValue)
{
final LabelValueChoiceRenderer<Integer> pageSizeChoiceRenderer = new LabelValueChoiceRenderer<Integer>();
final NumberFormat nf = NumberFormat.getInstance(locale);
for (final int size : PAGE_SIZES) {
if (size >= minValue && size <= maxValue) {
pageSizeChoiceRenderer.addValue(size, nf.format(size));
}
}
final DropDownChoice<Integer> pageSizeChoice = new DropDownChoice<Integer>(id, model, pageSizeChoiceRenderer.getValues(),
pageSizeChoiceRenderer);
pageSizeChoice.setNullValid(false);
return pageSizeChoice;
}
public AbstractListForm(final P parentPage)
{
super(parentPage);
getSearchFilter();
}
@SuppressWarnings("serial")
@Override
protected void init()
{
super.init();
final FeedbackPanel feedbackPanel = new FeedbackPanel("feedback");
feedbackPanel.setOutputMarkupId(true);
add(feedbackPanel);
gridBuilder = newGridBuilder(this, "filter");
if (isFilterVisible() == true) {
{
// Fieldset search filter
final FieldsetPanel fs = gridBuilder.newFieldset(getString("searchFilter"));
if (parentPage.getBaseDao().isHistorizable() == true) {
IconPanel icon = new IconPanel(fs.newIconChildId(), IconType.PLUS_SIGN, getString("filter.extendedSearch"))
.setOnClick("javascript:showExtendedFilter();");
icon.setMarkupId("showExtendedFilter");
fs.add(icon, FieldSetIconPosition.BOTTOM_LEFT);
icon = new IconPanel(fs.newIconChildId(), IconType.MINUS_SIGN).setOnClick("javascript:hideExtendedFilter();");
icon.setMarkupId("hideExtendedFilter");
fs.add(icon, FieldSetIconPosition.BOTTOM_LEFT);
}
final TextField< ? > searchTextField = createSearchTextField();
fs.add(searchTextField);
fs.setLabelFor(searchTextField);
final Model<String> modifiedSearchExpressionModel = new Model<String>() {
@Override
public String getObject()
{
return getModifiedSearchExpressionLabel(AbstractListForm.this, searchFilter.getSearchString());
}
};
final DivPanel div = new DivPanel(fs.newChildId()) {
/**
* @see org.projectforge.web.wicket.flowlayout.DivPanel#isVisible()
*/
@Override
public boolean isVisible()
{
return StringUtils.isNotBlank(searchFilter.getSearchString()) == true;
}
};
div.add(AttributeModifier.append("class", "modifiedSearchExpressionLabel"));
fs.add(div);
modifiedSearchExpressionLabel = new Label(div.newChildId(), modifiedSearchExpressionModel);
modifiedSearchExpressionLabel.setEscapeModelStrings(false);
div.add(modifiedSearchExpressionLabel);
fs.addHelpIcon(getString("tooltip.lucene.link"), FieldSetIconPosition.TOP_RIGHT).setOnClickLocation(getRequestCycle(),
WebConstants.DOC_LINK_HANDBUCH_LUCENE, true);
final String helpKeyboardImageTooltip = getHelpKeyboardImageTooltip();
if (helpKeyboardImageTooltip != null) {
fs.addKeyboardHelpIcon(helpKeyboardImageTooltip);
}
}
}
if (parentPage.getBaseDao().isHistorizable() == true && isFilterVisible() == true) {
addExtendedFilter();
}
if (showOptionsPanel() == true) {
gridBuilder.newSplitPanel(GridSize.COL66);
optionsFieldsetPanel = gridBuilder.newFieldset(getOptionsLabel()).suppressLabelForWarning();
final DivPanel optionsCheckBoxesPanel = optionsFieldsetPanel.addNewCheckBoxButtonDiv();
onOptionsPanelCreate(optionsFieldsetPanel, optionsCheckBoxesPanel);
if (showHistorySearchAndDeleteCheckbox() == true) {
optionsCheckBoxesPanel.add(createAutoRefreshCheckBoxButton(optionsCheckBoxesPanel.newChildId(),
new PropertyModel<Boolean>(getSearchFilter(), "deleted"), getString(I18N_ONLY_DELETED), getString(I18N_ONLY_DELETED_TOOLTIP))
.setWarning());
optionsCheckBoxesPanel.add(createAutoRefreshCheckBoxButton(optionsCheckBoxesPanel.newChildId(), new PropertyModel<Boolean>(
getSearchFilter(), "searchHistory"), getString("search.searchHistory"), getString("search.searchHistory.additional.tooltip")));
}
if (optionsCheckBoxesPanel.hasChilds() == true) {
optionsFieldsetPanel.add(optionsCheckBoxesPanel);
}
gridBuilder.newSplitPanel(GridSize.COL33);
} else {
gridBuilder.newGridPanel();
}
// DropDownChoice page size
pageSizeFieldsetPanel = gridBuilder.newFieldset(getString("label.pageSize"));
pageSizeFieldsetPanel.add(getPageSizeDropDownChoice(pageSizeFieldsetPanel.getDropDownChoiceId(), getLocale(), new PropertyModel<Integer>(this, "pageSize"), 25, 1000));
final WebMarkupContainer buttonCell = new WebMarkupContainer("buttonCell");
add(buttonCell);
actionButtons = new MyComponentsRepeater<Component>("actionButtons");
buttonCell.add(actionButtons.getRepeatingView());
final Button cancelButton = new Button("button", new Model<String>("cancel")) {
@Override
public final void onSubmit()
{
getParentPage().onCancelSubmit();
}
};
cancelButton.setDefaultFormProcessing(false);
cancelButtonPanel = new SingleButtonPanel(getNewActionButtonChildId(), cancelButton, getString("cancel"), SingleButtonPanel.CANCEL);
addActionButton(cancelButtonPanel);
final Button resetButton = new Button("button", new Model<String>("reset")) {
@Override
public final void onSubmit()
{
getParentPage().onResetSubmit();
}
};
resetButton.setDefaultFormProcessing(false);
resetButtonPanel = new SingleButtonPanel(getNewActionButtonChildId(), resetButton, getString("reset"), SingleButtonPanel.RESET);
addActionButton(resetButtonPanel);
final Button nextButton = new Button("button", new Model<String>("next")) {
@Override
public final void onSubmit()
{
getParentPage().onNextSubmit();
}
};
nextButtonPanel = new SingleButtonPanel(getNewActionButtonChildId(), nextButton, getString("next"), SingleButtonPanel.DEFAULT_SUBMIT);
addActionButton(nextButtonPanel);
final Button searchButton = new Button("button", new Model<String>("search")) {
@Override
public final void onSubmit()
{
getParentPage().onSearchSubmit();
}
};
searchButtonPanel = new SingleButtonPanel(getNewActionButtonChildId(), searchButton, getString("search"),
SingleButtonPanel.DEFAULT_SUBMIT);
addActionButton(searchButtonPanel);
setComponentsVisibility();
}
protected String getOptionsLabel()
{
return getString("label.options");
}
@SuppressWarnings("serial")
private void addExtendedFilter()
{
gridBuilder.newSplitPanel(GridSize.COL66);
extendedFilter = gridBuilder.getRowPanel();
extendedFilter.setMarkupId("extendedFilter");
if (searchFilter.isUseModificationFilter() == false) {
extendedFilter.add(AttributeModifier.append("style", "display: none;"));
}
{
final FieldsetPanel fieldset = gridBuilder.newFieldset(getString("search.periodOfModification"));
fieldset.add(new HiddenInputPanel(fieldset.newChildId(), new HiddenField<Boolean>(InputPanel.WICKET_ID, new PropertyModel<Boolean>(
searchFilter, "useModificationFilter"))).setHtmlId("useModificationFilter"));
startDateTimePanel = new DateTimePanel(fieldset.newChildId(), new PropertyModel<Date>(searchFilter, "startTimeOfModification"),
(DateTimePanelSettings) DateTimePanelSettings.get().withSelectProperty("startDateOfModification").withSelectPeriodMode(true),
DatePrecision.MINUTE);
fieldset.add(startDateTimePanel);
fieldset.setLabelFor(startDateTimePanel);
stopDateTimePanel = new DateTimePanel(fieldset.newChildId(), new PropertyModel<Date>(searchFilter, "stopTimeOfModification"),
(DateTimePanelSettings) DateTimePanelSettings.get().withSelectProperty("stopDateOfModification").withSelectPeriodMode(true),
DatePrecision.MINUTE);
stopDateTimePanel.setRequired(false);
fieldset.add(stopDateTimePanel);
final HtmlCommentPanel comment = new HtmlCommentPanel(fieldset.newChildId(), new DatesAsUTCModel() {
@Override
public Date getStartTime()
{
return searchFilter.getStartTimeOfModification();
}
@Override
public Date getStopTime()
{
return searchFilter.getStopTimeOfModification();
}
});
fieldset.add(comment);
// DropDownChoice for convenient selection of time periods.
final LabelValueChoiceRenderer<String> timePeriodChoiceRenderer = new LabelValueChoiceRenderer<String>();
timePeriodChoiceRenderer.addValue("lastMinute", getString("search.lastMinute"));
timePeriodChoiceRenderer.addValue("lastMinutes:10", PFUserContext.getLocalizedMessage("search.lastMinutes", 10));
timePeriodChoiceRenderer.addValue("lastMinutes:30", PFUserContext.getLocalizedMessage("search.lastMinutes", 30));
timePeriodChoiceRenderer.addValue("lastHour", getString("search.lastHour"));
timePeriodChoiceRenderer.addValue("lastHours:4", PFUserContext.getLocalizedMessage("search.lastHours", 4));
timePeriodChoiceRenderer.addValue("today", getString("search.today"));
timePeriodChoiceRenderer.addValue("sinceYesterday", getString("search.sinceYesterday"));
timePeriodChoiceRenderer.addValue("lastDays:3", PFUserContext.getLocalizedMessage("search.lastDays", 3));
timePeriodChoiceRenderer.addValue("lastDays:7", PFUserContext.getLocalizedMessage("search.lastDays", 7));
timePeriodChoiceRenderer.addValue("lastDays:14", PFUserContext.getLocalizedMessage("search.lastDays", 14));
timePeriodChoiceRenderer.addValue("lastDays:30", PFUserContext.getLocalizedMessage("search.lastDays", 30));
timePeriodChoiceRenderer.addValue("lastDays:60", PFUserContext.getLocalizedMessage("search.lastDays", 60));
timePeriodChoiceRenderer.addValue("lastDays:90", PFUserContext.getLocalizedMessage("search.lastDays", 90));
final DropDownChoice<String> modificationSinceChoice = new DropDownChoice<String>(fieldset.getDropDownChoiceId(),
new PropertyModel<String>(this, "modificationSince"), timePeriodChoiceRenderer.getValues(), timePeriodChoiceRenderer);
modificationSinceChoice.setNullValid(true);
modificationSinceChoice.setRequired(false);
fieldset.add(modificationSinceChoice, true);
}
{
gridBuilder.newSplitPanel(GridSize.COL33);
final FieldsetPanel fs = gridBuilder.newFieldset(getString("modifiedBy"), getString("user"));
final UserSelectPanel userSelectPanel = new UserSelectPanel(fs.newChildId(), new Model<PFUserDO>() {
@Override
public PFUserDO getObject()
{
return userGroupCache.getUser(searchFilter.getModifiedByUserId());
}
@Override
public void setObject(final PFUserDO object)
{
if (object == null) {
searchFilter.setModifiedByUserId(null);
} else {
searchFilter.setModifiedByUserId(object.getId());
}
}
}, parentPage, "modifiedByUserId");
fs.add(userSelectPanel);
userSelectPanel.setDefaultFormProcessing(false);
userSelectPanel.init().withAutoSubmit(true);
}
gridBuilder.setCurrentLevel(0); // Go back to main row panel.
}
/**
* Here you can add elements to the given FieldsetPanel or optionsPanel. The optionsPanel is not yet added to the FieldsetPanel.
* @param optionsFieldsetPanel
* @param optionsPanel
*/
protected void onOptionsPanelCreate(final FieldsetPanel optionsFieldsetPanel, final DivPanel optionsCheckBoxesPanel)
{
}
/**
* Creates a simple TextField and sets the focus on it. Overwrite this method if you want to add for example an auto completion text field
* (ajax). Please don't forget to call addSearchFieldTooltip() in your method!
*/
protected TextField< ? > createSearchTextField()
{
final TextField<String> searchField = new TextField<String>(InputPanel.WICKET_ID, new PropertyModel<String>(getSearchFilter(),
"searchString"));
createSearchFieldTooltip(searchField);
searchField.add(WicketUtils.setFocus());
return searchField;
}
protected void createSearchFieldTooltip(final Component field)
{
WicketUtils.addTooltip(field, getString("search.string.info.title"), getParentPage().getSearchToolTip(), false);
}
public void addActionButton(final Component entry)
{
this.actionButtons.add(entry);
}
public void prependActionButton(final Component entry)
{
this.actionButtons.add(0, entry);
}
public String getNewActionButtonChildId()
{
return this.actionButtons.newChildId();
}
@Override
public void onBeforeRender()
{
super.onBeforeRender();
actionButtons.render();
}
protected void setComponentsVisibility()
{
if (parentPage.isMassUpdateMode() == true) {
cancelButtonPanel.setVisible(true);
searchButtonPanel.setVisible(false);
resetButtonPanel.setVisible(false);
nextButtonPanel.setVisible(true);
setDefaultButton(nextButtonPanel.getButton());
} else {
if (parentPage.isSelectMode() == false) {
// Show cancel button only in select mode.
cancelButtonPanel.setVisible(false);
}
searchButtonPanel.setVisible(true);
resetButtonPanel.setVisible(true);
nextButtonPanel.setVisible(false);
setDefaultButton(searchButtonPanel.getButton());
}
}
/**
* onchange, onclick or submit without button.
* @see org.apache.wicket.markup.html.form.Form#onSubmit()
*/
@Override
protected void onSubmit()
{
super.onSubmit();
if (modificationSince != null) {
final int pos = modificationSince.indexOf(':');
final Integer number;
if (pos >= 0) {
number = NumberHelper.parseInteger(modificationSince.substring(pos + 1));
} else {
number = null;
}
final DateHolder dateHolder = new DateHolder(DatePrecision.MINUTE);
if ("lastMinute".equals(modificationSince) == true) {
dateHolder.add(Calendar.MINUTE, -1);
} else if (modificationSince.startsWith("lastMinutes:") == true) {
dateHolder.add(Calendar.MINUTE, -number);
} else if ("lastHour".equals(modificationSince) == true) {
dateHolder.add(Calendar.HOUR, -1);
} else if (modificationSince.startsWith("lastHours:") == true) {
dateHolder.add(Calendar.HOUR, -number);
} else if ("today".equals(modificationSince) == true) {
dateHolder.setBeginOfDay();
} else if ("sinceYesterday".equals(modificationSince) == true) {
dateHolder.add(Calendar.DAY_OF_YEAR, -1);
dateHolder.setBeginOfDay();
} else if (modificationSince.startsWith("lastDays") == true) {
dateHolder.add(Calendar.DAY_OF_YEAR, -number);
dateHolder.setBeginOfDay();
}
searchFilter.setStartTimeOfModification(dateHolder.getDate());
startDateTimePanel.markModelAsChanged();
searchFilter.setStopTimeOfModification(null);
stopDateTimePanel.markModelAsChanged();
modificationSince = null;
}
getParentPage().onSearchSubmit();
}
@SuppressWarnings("unchecked")
public F getSearchFilter()
{
if (this.searchFilter != null) {
return this.searchFilter;
} else {
if (getParentPage().isStoreFilter() == true) {
final Object filter = getParentPage().getUserPrefEntry(this.getClass().getName() + ":Filter");
if (filter != null) {
if (filter.getClass().equals(newSearchFilterInstance().getClass()) == true) {
try {
this.searchFilter = (F) filter;
} catch (final ClassCastException ex) {
// No output needed, info message follows:
}
if (this.searchFilter == null) {
// Probably a new software release results in an incompability of old and new filter format.
getLogger().info(
"Could not restore filter from user prefs: (old) filter type "
+ filter.getClass().getName()
+ " is not assignable to (new) filter type "
+ newSearchFilterInstance().getClass().getName()
+ " (OK, probably new software release).");
}
}
}
}
}
if (this.searchFilter == null) {
this.searchFilter = newSearchFilterInstance();
this.searchFilter.reset();
if (getParentPage().isStoreFilter() == true) {
getParentPage().putUserPrefEntry(this.getClass().getName() + ":Filter", this.searchFilter, true);
}
}
return this.searchFilter;
}
public CheckBoxButton createAutoRefreshCheckBoxButton(final String id, final IModel<Boolean> model, final String label)
{
return createAutoRefreshCheckBoxButton(id, model, label, null);
}
@SuppressWarnings("serial")
protected CheckBoxButton createAutoRefreshCheckBoxButton(final String id, final IModel<Boolean> model, final String label,
final String tooltip)
{
final CheckBoxButton checkBoxPanel = new CheckBoxButton(id, model, label) {
@Override
protected boolean wantOnSelectionChangedNotifications()
{
return true;
};
@Override
protected void onSelectionChanged(final Boolean newSelection)
{
parentPage.refresh();
};
};
if (tooltip != null) {
checkBoxPanel.setTooltip(tooltip);
}
return checkBoxPanel;
}
/**
* The page size of display tag (result table).
*/
public Integer getPageSize()
{
if (pageSize == null) {
pageSize = (Integer) getParentPage().getUserPrefEntry(this.getClass().getName() + ":pageSize");
}
if (pageSize == null) {
pageSize = 50;
}
return pageSize;
}
/**
* For convenience combo box with quick select of often used time periods.
*/
public String getModificationSince()
{
return modificationSince;
}
public void setModificationSince(final String modificationSince)
{
this.modificationSince = modificationSince;
}
public void setPageSize(final Integer pageSize)
{
this.pageSize = pageSize;
if (getParentPage().isStoreFilter() == true) {
getParentPage().putUserPrefEntry(this.getClass().getName() + ":pageSize", this.pageSize, true);
}
}
/**
* For displaying the modified search string for lucene, e. g. "modified searchstring: micromata*"
* @param component Needed for {@link Component#getString(String)}.
* @param searchString
* @return
*/
public static String getModifiedSearchExpressionLabel(final Component component, final String searchString)
{
return component.getString("search.lucene.expression") + " " + StringEscapeUtils.escapeHtml(BaseDao.modifySearchString(searchString));
}
/**
* Any given de-serialized filter will be set from parent page.
* @param filter
*/
@SuppressWarnings("unchecked")
void setFilter(final Object filter)
{
searchFilter = (F) filter;
}
void copySearchFieldsFrom(final BaseSearchFilter baseFilter)
{
searchFilter.copyBaseSearchFieldsFrom(baseFilter);
}
/**
* Used by search cell to define visibility.
* @return True if not overload.
*/
protected boolean isFilterVisible()
{
return true;
}
/**
* Used by search cell to define visibility of search input string and extended search filter.
* @return True if not overload.
*/
protected boolean isSearchFilterVisible()
{
return true;
}
/**
* If the derived class returns a text, the keyboard image right to the search field will be shown with the returned string as tool-tip. <br/>
* If the derived class uses the store-recent-search-terms-functionality then a generic tool-tip about this functionality is used.<br/>
* Otherwise the image is invisible (default).
*/
protected String getHelpKeyboardImageTooltip()
{
if (parentPage.isRecentSearchTermsStorage() == true) {
return getString("tooltip.autocomplete.recentSearchTerms");
} else {
return null;
}
}
/** This class uses the logger of the extended class. */
protected abstract Logger getLogger();
protected boolean showOptionsPanel()
{
return parentPage.getBaseDao().isHistorizable();
}
protected boolean showHistorySearchAndDeleteCheckbox()
{
return parentPage.getBaseDao().isHistorizable();
}
}