Java tutorial
/** * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. * If a copy of the MPL was not distributed with this file, You can obtain one at * http://mozilla.org/MPL/2.0/. * * This Source Code Form is also subject to the terms of the Health-Related Additional * Disclaimer of Warranty and Limitation of Liability available at * http://www.carewebframework.org/licensing/disclaimer. */ package org.carewebframework.cal.ui.reporting.controller; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Date; import java.util.List; import ca.uhn.fhir.model.dstu.resource.Patient; import ca.uhn.fhir.model.dstu.resource.User; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.carewebframework.api.event.IGenericEvent; import org.carewebframework.api.property.PropertyUtil; import org.carewebframework.cal.api.patient.PatientContext; import org.carewebframework.cal.api.query.AbstractServiceContext.DateMode; import org.carewebframework.cal.api.query.IDataFilter; import org.carewebframework.cal.api.query.IDataService; import org.carewebframework.cal.api.query.IQueryResult; import org.carewebframework.cal.api.user.UserContext; import org.carewebframework.cal.ui.reporting.Constants; import org.carewebframework.cal.ui.reporting.Util; import org.carewebframework.cal.ui.reporting.model.ServiceContext; import org.carewebframework.common.DateRange; import org.carewebframework.common.DateUtil; import org.carewebframework.common.StrUtil; import org.carewebframework.shell.plugins.PluginController; import org.carewebframework.ui.thread.ZKThread; import org.carewebframework.ui.zk.DateRangePicker; import org.carewebframework.ui.zk.HybridModel; import org.carewebframework.ui.zk.HybridModel.IGrouper; import org.carewebframework.ui.zk.ListUtil; import org.carewebframework.ui.zk.ZKUtil; import org.springframework.beans.BeanUtils; import org.zkoss.util.resource.Labels; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.event.Events; import org.zkoss.zul.Button; import org.zkoss.zul.Combobox; import org.zkoss.zul.Comboitem; import org.zkoss.zul.GroupsModel; import org.zkoss.zul.Label; import org.zkoss.zul.ListModel; import org.zkoss.zul.impl.MeshElement; /** * This is a stateful controller that supports plugins that use a list model and background thread * for data retrieval. It supports paging vs ROD-based views. * * @param <T> DTO class returned by service. */ public abstract class AbstractController<T> extends PluginController { private static final long serialVersionUID = 1L; private static final Log log = LogFactory.getLog(AbstractController.class); private static final String EVENT_PATIENT_CHANGE = "CONTEXT.CHANGED.Patient"; private static final String ATTR_ROD_SIZE = "org.zkoss.zul.%.initRodSize"; private static final String ATTR_ROD = "org.zkoss.zul.%.rod"; protected final IDataFilter<T> dateRangeFilter = new IDataFilter<T>() { /** * Filter result based on selected date range. */ @Override public boolean include(T result) { return getDateRange().inRange(DateUtil.stripTime(getDate(result, queryDateMode)), true, true); } /** * Returns true if date mode has changed or if current date range falls outside cached data. */ @Override public boolean requiresFetch() { if (queryDateMode != getDateMode()) { return true; } DateRange dateRange = getDateRange(); return !queryDateRange.inRange(dateRange.getStartDate(), true, true) || !queryDateRange.inRange(dateRange.getEndDate(), true, true); } }; // These components are autowired by the controller. private DateRangePicker dateRangePicker; private Combobox dateModePicker; private Component printRoot; private Label lblMessage; private Button btnPagingToggle; // --- End of autowired section // Date range of last query private DateRange queryDateRange; private DateMode queryDateMode = DateMode.PHYSIOLOGIC; private final IDataService<T> service; private final HybridModel<T, Object> dataModel; private final String propertyPrefix; private final String labelPrefix; private final List<IDataFilter<T>> dataFilters = new ArrayList<IDataFilter<T>>(); private boolean fetchPending; private Patient patient; private User user; private Component hideOnShowMessage; private MeshElement meshElement; private String rodType; private int rodInitSize = 5; private boolean isPaging = true; // Maximum number of rows for paging view. private int pageSize = 30; private final String printStyleSheet; private final boolean usesPatient; private final IGrouper<T, ?> grouper; private final IGenericEvent<Object> patientContextListener = new IGenericEvent<Object>() { /** * Forces data refresh on patient context change. */ @Override public void eventCallback(String eventName, Object eventData) { AbstractController.this.patientChanged(); } }; /** * Create the controller. * * @param service The is the data query service. * @param labelPrefix Prefix used to resolve label id's with placeholders. * @param propertyPrefix Prefix for property names. * @param printStyleSheet Optional style sheet to apply when printing. * @param usesPatient If true, uses patient context. */ public AbstractController(IDataService<T> service, String labelPrefix, String propertyPrefix, String printStyleSheet, boolean usesPatient) { this(service, labelPrefix, propertyPrefix, printStyleSheet, usesPatient, null); } /** * Create the controller. * * @param service The is the data query service. * @param labelPrefix Prefix used to resolve label id's with placeholders. * @param propertyPrefix Prefix for property names. * @param printStyleSheet Optional style sheet to apply when printing. * @param usesPatient If true, uses patient context. * @param grouper The grouper implementation, or null if the data is not grouped. */ @SuppressWarnings("unchecked") public AbstractController(IDataService<T> service, String labelPrefix, String propertyPrefix, String printStyleSheet, boolean usesPatient, IGrouper<T, ?> grouper) { super(); this.service = service; this.labelPrefix = labelPrefix; this.propertyPrefix = propertyPrefix; if (printStyleSheet != null && !printStyleSheet.startsWith("~./")) { printStyleSheet = ZKUtil.getResourcePath(getClass()) + printStyleSheet; } this.printStyleSheet = printStyleSheet; this.usesPatient = usesPatient; this.grouper = grouper; this.dataModel = new HybridModel<T, Object>((IGrouper<T, Object>) grouper); } /** * Returns the date for the given result for filtering purposes. * * @param result Result from which to extract a date. * @param dateMode The date mode. * @return The extracted date. */ protected abstract Date getDate(T result, DateMode dateMode); /** * Subclass should implement to set list model into appropriate component. * * @param model The list model. */ protected abstract void setListModel(ListModel<T> model); /** * Subclass should implement to set groups model into appropriate component. * * @param model The groups model. */ protected abstract void setGroupsModel(GroupsModel<T, ?, ?> model); /** * Sets the mesh element that consumes the list model (a grid or listbox). * * @param meshElement The mesh element. * @param rodType The ROD setting. */ /*package*/void setMeshElement(MeshElement meshElement, String rodType) { this.meshElement = meshElement; this.rodType = rodType; setHideOnShowMessage(meshElement); } protected IGrouper<T, ?> getGrouper() { return grouper; } /** * Registers a data filter, if any. * * @param dataFilter The secondary data filter. */ protected void registerDataFilter(IDataFilter<T> dataFilter) { dataFilters.add(dataFilter); } /** * Unregisters a secondary data filter. * * @param dataFilter The secondary data filter. */ protected void unregisterDataFilter(IDataFilter<T> dataFilter) { dataFilters.remove(dataFilter); } /** * The list model has been updated. If the model is empty or null, displays the no data message. * This ultimately calls the abstract setModel method to allow the subclass to handle the * updated model. * * @param model The hybrid model. */ private void updateModel(HybridModel<T, ?> model) { if (model == null || model.isEmpty()) { String msg = getLabel(Constants.LABEL_ID_NO_DATA); log.trace(msg); showMessage(msg); } else { showMessage(null); } if (model.isGrouped()) { setGroupsModel(model); } else { setListModel(model); } if (isPaging) { this.meshElement.setActivePage(0); } } /** * Returns the unfiltered list model. * * @return The unfiltered list model. */ protected ListModel<T> getModel() { return dataModel; } /** * Retrieves a property value of the specified data type. It examines the property value for a * type-compatible value. Failing that, it returns the specified default value. * * @param propName Name of property from which to retrieve the value. * @param clazz Expected data type of the property value. * @param dflt Default value to use if a suitable one cannot be found. * @return The value. */ @SuppressWarnings("unchecked") protected <V> V getPropertyValue(String propName, Class<V> clazz, V dflt) { V value = null; if (propName != null) { propName = propName.replace("%", propertyPrefix == null ? "" : propertyPrefix); String val = StringUtils.trimToNull(PropertyUtil.getValue(propName)); if (log.isDebugEnabled()) { log.debug("Property " + propName + " value: " + val); } if (clazz == String.class) { value = (V) (val); } else { Method method = BeanUtils.findMethod(clazz, "valueOf", String.class); if (method != null && method.getReturnType() == clazz) { value = (V) parseString(method, val, null); } } } return value == null ? dflt : value; } /** * Uses the valueOf method in the target type class to convert one of two candidate values to * the target type. Failing that, it returns null. * * @param method The valueOf method in the target class. * @param value1 The first candidate value to try. * @param value2 The second candidate value to try. * @return The converted value if successful; null if not. */ private Object parseString(Method method, String value1, String value2) { try { return method.invoke(null, value1); } catch (Exception e) { return value2 == null ? null : parseString(method, value2, null); } } /** * Creates a new empty list model. Ensures that the setting of the multiple property is * maintained from the original list model. * * @return A new list model. */ private HybridModel<T, Object> newModel() { return new HybridModel<T, Object>(dataModel); } /** * If there is a deferred fetch operation when the plugin is activated, invoke it now. */ @Override public void onActivate() { super.onActivate(); if (this.fetchPending) { log.trace("Processing deferred data request."); fetchData(); } } /** * Unsubscribe context listener on unload. */ @Override public void onUnload() { super.onUnload(); if (usesPatient) { getEventManager().unsubscribe(EVENT_PATIENT_CHANGE, patientContextListener); } } /** * Initializes Controller. */ protected void initializeController() { log.trace("Initializing Controller"); lblMessage.setZclass("z-toolbar"); rodInitSize = getPropertyValue(Constants.PROPERTY_ID_ROD_SIZE, Integer.class, rodInitSize); pageSize = getPropertyValue(Constants.PROPERTY_ID_MAX_ROWS, Integer.class, pageSize); onUpdatePaging(); if (dateRangePicker != null) { String deflt = getPropertyValue(Constants.PROPERTY_ID_DATE_RANGE, String.class, "Last Two Years"); dateRangePicker.setSelectedItem(dateRangePicker.findMatchingItem(deflt)); registerDataFilter(dateRangeFilter); } if (dateModePicker != null) { for (DateMode dm : DateMode.values()) { String lbl = getLabel(Constants.LABEL_ID_DATEMODE.replace("$", dm.name().toLowerCase())); Comboitem item = new Comboitem(lbl); item.setValue(dm); dateModePicker.appendChild(item); } DateMode sortModePref = getPropertyValue(Constants.PROPERTY_ID_SORT_MODE, DateMode.class, queryDateMode); int idx = ListUtil.findComboboxData(dateModePicker, sortModePref); dateModePicker.setSelectedIndex(idx == -1 ? 0 : idx); dateModePicker.setReadonly(true); } } /** * Updates component states according to paging mode. */ public void onUpdatePaging() { showBusy(null); if (this.meshElement != null) { if (this.isPaging) { this.meshElement.setMold("paging"); this.meshElement.setPageSize(this.pageSize); activateROD(false); } else { this.meshElement.setMold(null); activateROD(true); } } if (this.btnPagingToggle != null) { this.btnPagingToggle .setLabel(getLabel(this.isPaging ? Constants.LABEL_ID_PAGE_OFF : Constants.LABEL_ID_PAGE_ON)); } } /** * Activates/deactivates ROD support. Note that setting the rodInitSize to zero or less will * always inhibit ROD support. * * @param activate True activates ROD; false deactivates it. */ private void activateROD(boolean activate) { this.meshElement.setAttribute(ATTR_ROD.replace("%", rodType), activate && this.rodInitSize > 0); this.meshElement.setAttribute(ATTR_ROD_SIZE.replace("%", rodType), this.rodInitSize); } /** * Returns a label value given its id. Will attempt to find a label for the current label * prefix. Failing that, will use the default label prefix. * * @param labelId Id of the label sought. * @return The label value, or the default value if none found. */ protected String getLabel(String labelId) { String label = getLabel(labelId, labelPrefix); return label != null ? label : getLabel(labelId, "reporting"); } /** * Returns a label value given its id. Recognizes placeholders in label names, replacing them * with the default label prefix. * * @param labelId Id of the label sought. * @param placeholder Placeholder value to substitute. * @return The label value, or the default value if none found. */ protected String getLabel(String labelId, String placeholder) { return placeholder == null ? null : Labels.getLabel(labelId.replace("%", placeholder)); } /** * Override to add additional parameters to service context. * * @return The service context. */ protected ServiceContext<T> getServiceContext() { return new ServiceContext<T>(service, user, patient, queryDateRange, queryDateMode); } /** * Evaluates the service context to determine if all required parameters are present. * * @param ctx Service context to examine. * @return Null if all required parameters are present. Otherwise, the id of a label to display. */ protected String hasRequired(ServiceContext<T> ctx) { return usesPatient && ctx.patient == null ? Constants.LABEL_ID_NO_PATIENT : null; } /** * Submits a data fetch request in the background. */ protected void fetchData() { log.trace("Submitting data fetch request."); this.fetchPending = false; abortBackgroundThreads(); showMessage(""); showBusy(getLabel(Constants.LABEL_ID_FETCHING)); this.queryDateRange = getDateRange(); this.queryDateMode = getDateMode(); this.dataModel.clear(); ServiceContext<T> ctx = getServiceContext(); String msg = hasRequired(ctx); if (msg == null) { log.trace("Starting background thread."); startBackgroundThread(ctx); } else { log.trace(msg); showMessage(getLabel(msg)); } } /** * Executed upon background thread completion, placing the query results into the list model. */ @SuppressWarnings("unchecked") @Override protected void threadFinished(ZKThread thread) { processResult((IQueryResult<T>) thread.getAttribute("result")); filterData(); try { thread.rethrow(); } catch (Throwable e) { log.error("Background thread threw an exception.", e); showMessage("@reporting.plugin.error.unexpected"); } } /** * Executed when the background thread is aborted. */ @Override protected void threadAborted(ZKThread thread) { showMessage("@reporting.plugin.status.aborted"); } /** * Process the query result. * * @param queryResult The query result to process. */ protected void processResult(IQueryResult<T> queryResult) { List<T> results = queryResult == null ? null : queryResult.getResults(); boolean isEmpty = results == null || results.isEmpty(); if (log.isDebugEnabled()) { log.debug("Query fetched " + (isEmpty ? "no" : results.size()) + " result(s)."); } this.dataModel.clear(); if (!isEmpty) { this.dataModel.addAll(results); } } /** * Event handler to handle changes in the DateMode of a query */ public void onSelect$dateModePicker() { log.trace("Handling onSelect of dateModePicker Combobox"); if (log.isDebugEnabled()) { log.debug("dateModePicker Val: " + this.dateModePicker.getSelectedItem().getValue()); } filterChanged(); } /** * The event handler for DatePicker events. Compares DatePicker range against cached date range. * If out of range, {@link #fetchData()} is called and cache is refreshed */ public void onSelectRange$dateRangePicker() { if (log.isTraceEnabled()) { log.trace("DatePicker selectRange event fired"); log.trace("DatePicker.Range: " + getDateRange()); } filterChanged(); } /** * A filter setting has changed. */ protected void filterChanged() { if (isFetchRequired()) { refresh(); // hit database } else { filterData(); } } /** * Filters the cached List, trimming it down to the records that satisfy the filter criteria. */ private void filterData() { log.trace("Filtering current list model given current criteria"); if (dataFilters.isEmpty()) { updateModel(this.dataModel); return; } final HybridModel<T, Object> filteredList = newModel(); for (final T result : this.dataModel) { boolean match = true; for (IDataFilter<T> dataFilter : dataFilters) { match = dataFilter.include(result); if (!match) { break; } } if (match) { filteredList.add(result); } } if (log.isDebugEnabled()) { log.debug("Cached records satisfied by filter criteria: " + filteredList.size()); } updateModel(dataModel.size() == filteredList.size() ? this.dataModel : filteredList); } /** * Returns true if current filter settings require a full data fetch. * * @return True if full data fetch is required. */ protected boolean isFetchRequired() { for (IDataFilter<T> dataFilter : dataFilters) { if (dataFilter.requiresFetch()) { return true; } } return false; } /** * Displays message to client. * * @param message Message to display to client. If null, message label is hidden. */ public void showMessage(String message) { showBusy(null); message = StrUtil.formatMessage(message); boolean show = message != null; if (lblMessage != null) { lblMessage.setVisible(show); lblMessage.setValue(show ? message : ""); } if (hideOnShowMessage != null) { hideOnShowMessage.setVisible(!show); } } /** * Overriding super class * * @see org.carewebframework.ui.FrameworkController#doAfterCompose(Component) * @param comp Component * @throws Exception thrown when error occurs */ @Override public void doAfterCompose(final Component comp) throws Exception { log.trace("doAfterCompose..."); super.doAfterCompose(comp); user = UserContext.getActiveUser().getNativeUser(); initializeController(); if (usesPatient) { getEventManager().subscribe(EVENT_PATIENT_CHANGE, patientContextListener); patientChanged(); } else { refresh(); } } /** * Refreshes the data by re-fetching from the data source. If the plugin is not active, the * fetch request is deferred. */ protected void patientChanged() { this.patient = PatientContext.getActivePatient(); refresh(); } /** * Refreshes the data by re-fetching from the data source. If the plugin is not active, the * fetch request is deferred. */ @Override public void refresh() { log.trace("Refreshing view."); abortBackgroundThreads(); if (isActive()) { fetchData(); } else { this.fetchPending = true; } } /** * Returns date range from picker. * * @return The date range. */ protected DateRange getDateRange() { return this.dateRangePicker == null ? null : this.dateRangePicker.getSelectedRange(); } /** * Returns date mode from picker. * * @return The date mode. */ protected DateMode getDateMode() { Comboitem item = this.dateModePicker == null ? null : this.dateModePicker.getSelectedItem(); return item == null ? queryDateMode : (DateMode) item.getValue(); } /** * Invoke refresh upon refresh button click. */ public void onClick$btnRefresh() { refresh(); } /** * Paging Toggle */ public void onClick$btnPagingToggle() { log.trace("Paging Toggle Button"); isPaging = !isPaging; showBusy(getLabel(Constants.LABEL_ID_WAITING)); Events.echoEvent("onUpdatePaging", root, isPaging); } protected void print(Component root) { String printTitle = getLabel(Constants.LABEL_ID_TITLE); Util.print(root == null ? meshElement.getParent() : root, printTitle, usesPatient ? "patient" : "user", printStyleSheet, false); }; public void onClick$btnPrint() { print(printRoot); } public boolean isPaging() { return isPaging; } public void setPaging(boolean value) { if (isPaging != value) { isPaging = value; if (meshElement != null) { onUpdatePaging(); } } } public boolean isMultiple() { return dataModel.isMultiple(); } public void setMultiple(boolean multiple) { dataModel.setMultiple(multiple); } /** * Returns the component to be hidden when a status message is displayed. May be null. * * @return The hide-on-show-message setting. */ public Component getHideOnShowMessage() { return hideOnShowMessage; } /** * Sets the component to be hidden when a status message is displayed. May be null. * * @param hideOnShowMessage The hide-on-show-message setting. */ public void setHideOnShowMessage(Component hideOnShowMessage) { this.hideOnShowMessage = hideOnShowMessage; } }