org.carewebframework.cal.ui.reporting.controller.AbstractController.java Source code

Java tutorial

Introduction

Here is the source code for org.carewebframework.cal.ui.reporting.controller.AbstractController.java

Source

/**
 * 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;
    }

}