/*
* #%L
* carewebframework
* %%
* Copyright (C) 2008 - 2016 Regenstrief Institute, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* 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.
*
* #L%
*/
package org.carewebframework.shell.plugins;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.WordUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.carewebframework.api.spring.SpringUtil;
import org.carewebframework.common.StrUtil;
import org.carewebframework.shell.CareWebShell;
import org.carewebframework.shell.CareWebUtil;
import org.carewebframework.shell.Constants;
import org.carewebframework.shell.layout.UIElementBase;
import org.carewebframework.shell.layout.UIElementZKBase;
import org.carewebframework.shell.plugins.PluginEvent.PluginAction;
import org.carewebframework.shell.property.PropertyInfo;
import org.carewebframework.ui.FrameworkController;
import org.carewebframework.ui.command.CommandEvent;
import org.carewebframework.ui.command.CommandUtil;
import org.carewebframework.ui.zk.ZKUtil;
import org.springframework.util.StringUtils;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.ext.Disable;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zul.Idspace;
/**
* Container that manages CareWeb plugins
*/
public class PluginContainer extends Idspace {
private static final long serialVersionUID = 1L;
private static final Log log = LogFactory.getLog(PluginContainer.class);
/**
* Used to hold property values prior to plugin initialization. When a plugin is subsequently
* initialized and registers a property, the value in the corresponding proxy is used to
* initialize the property. This allows the deserializer to initialize property values even
* though the plug-in has not yet been instantiated.
*/
private class PropertyProxy {
private Object value;
private final PropertyInfo propInfo;
private PropertyProxy(PropertyInfo propInfo, Object value) {
this.propInfo = propInfo;
this.value = value;
}
};
private final CareWebShell shell;
private PluginDefinition definition;
private ToolbarContainer tbarContainer;
private List<IPluginEvent> pluginEventListeners1;
private List<IPluginEventListener> pluginEventListeners2;
private List<Component> registeredComponents;
private List<Disable> registeredActions;
private Map<String, Object> registeredProperties;
private Map<String, Object> registeredBeans;
private boolean disabled;
private boolean destroying;
private boolean initialized;
private String busyMessage;
private boolean busyPending;
private boolean busyDisabled;
private String color;
private class ToolbarContainer extends Idspace {
private static final long serialVersionUID = 1L;
public ToolbarContainer() {
super();
setZclass("cwf-toolbar-container");
}
}
/**
* Returns the plugin container for the given component.
*
* @param comp The component whose hosting container is sought.
* @return The hosting plugin container, or null if no container hosts the component.
*/
public static PluginContainer getContainer(Component comp) {
return ZKUtil.findAncestor(comp, PluginContainer.class);
}
/**
* Create the plugin container.
*/
public PluginContainer() {
super();
shell = CareWebUtil.getShell();
setZclass("cwf-plugin-container");
setVisible(false);
setHeight("100%");
setWidth("100%");
}
/**
* Activate the plugin.
*/
public void activate() {
load();
executeAction(PluginAction.ACTIVATE, true);
setVisible(true);
}
/**
* Inactivate the plugin.
*/
public void inactivate() {
setVisible(false);
executeAction(PluginAction.INACTIVATE, true);
}
/**
* Release contained resources.
*/
public void destroy() {
if (!destroying) {
destroying = true;
shell.unregisterPlugin(this);
executeAction(PluginAction.UNLOAD, false);
CommandUtil.dissociateAll(this);
if (pluginEventListeners1 != null) {
pluginEventListeners1.clear();
pluginEventListeners1 = null;
}
if (pluginEventListeners2 != null) {
executeAction(PluginAction.UNSUBSCRIBE, false);
pluginEventListeners2.clear();
pluginEventListeners2 = null;
}
if (registeredProperties != null) {
registeredProperties.clear();
registeredProperties = null;
}
if (registeredBeans != null) {
registeredBeans.clear();
registeredBeans = null;
}
if (registeredComponents != null) {
for (Component component : registeredComponents) {
component.detach();
}
registeredComponents.clear();
registeredComponents = null;
}
}
}
/**
* Calls the hosting UI element to bring the plugin to the front of the UI.
*/
public void bringToFront() {
UIElementBase host = getHost();
if (host != null) {
host.bringToFront();
}
}
/**
* Returns the UI element hosting this container.
*
* @return The hosting UI element (could be null).
*/
public UIElementBase getHost() {
return UIElementZKBase.getAssociatedUIElement(this);
}
/**
* Sets the visibility of the contained resource and any registered components.
*
* @param visible Visibility state to set
*/
@Override
public boolean setVisible(boolean visible) {
boolean result = super.setVisible(visible);
if (result != visible && registeredComponents != null) {
for (Component component : registeredComponents) {
if (!visible) {
component.setAttribute(Constants.ATTR_VISIBLE, component.isVisible());
component.setVisible(false);
} else {
component.setVisible((Boolean) component.getAttribute(Constants.ATTR_VISIBLE));
}
}
}
if (visible) {
checkBusy();
}
return result;
}
/**
* Returns true if any plugin event listeners are registered.
*
* @return True if any plugin event listeners are registered.
*/
private boolean hasListeners() {
return pluginEventListeners1 != null || pluginEventListeners2 != null;
}
/**
* Notify all plugin callbacks of the specified action.
*
* @param action Action to perform.
* @param async If true, callbacks are done asynchronously.
*/
private void executeAction(PluginAction action, boolean async) {
executeAction(action, async, null);
}
/**
* Notify all plugin callbacks of the specified action.
*
* @param action Action to perform.
* @param data Event-dependent data (may be null).
* @param async If true, callbacks are done asynchronously.
*/
private void executeAction(PluginAction action, boolean async, Object data) {
if (hasListeners() || action == PluginAction.LOAD) {
PluginEvent event = new PluginEvent(this, action, data);
if (async) {
Events.postEvent(event);
} else {
onAction(event);
}
}
}
/**
* Notify listeners of plugin events.
*
* @param event The plugin event containing the action.
*/
public void onAction(PluginEvent event) {
PluginLifecycleEventException exception = null;
PluginAction action = event.getAction();
boolean debug = log.isDebugEnabled();
if (pluginEventListeners1 != null) {
for (IPluginEvent listener : new ArrayList<>(pluginEventListeners1)) {
try {
if (debug) {
log.debug("Invoking IPluginEvent.on" + WordUtils.capitalizeFully(action.name()) + " for listener "
+ listener);
}
switch (action) {
case LOAD:
listener.onLoad(this);
continue;
case UNLOAD:
listener.onUnload();
continue;
case ACTIVATE:
listener.onActivate();
continue;
case INACTIVATE:
listener.onInactivate();
continue;
}
} catch (Throwable e) {
exception = createChainedException(action.name(), e, exception);
}
}
}
if (pluginEventListeners2 != null) {
for (IPluginEventListener listener : new ArrayList<>(pluginEventListeners2)) {
try {
if (debug) {
log.debug("Delivering " + action.name() + " event to IPluginEventListener listener " + listener);
}
listener.onPluginEvent(event);
} catch (Throwable e) {
exception = createChainedException(action.name(), e, exception);
}
}
}
if (action == PluginAction.LOAD) {
doAfterLoad();
}
if (exception != null) {
throw exception;
}
}
/**
* Actions to perform after the container is loaded.
*/
protected void doAfterLoad() {
registerProperty(this, "color", false);
}
/**
* Forward onCommand events to first level children of the container.
*
* @param event The command event.
*/
public void onCommand(CommandEvent event) {
if (!disabled) {
for (Component child : this.getChildren()) {
Events.sendEvent(child, event);
if (!event.isPropagatable()) {
break;
}
}
}
}
/**
* Creates a chained exception.
*
* @param action Action being performed at the time of the exception.
* @param newException Exception just thrown.
* @param previousException Previous exception (may be null).
* @return Top level exception in chain.
*/
private PluginLifecycleEventException createChainedException(String action, Throwable newException,
PluginLifecycleEventException previousException) {
String msg = action + " event generated an error.";
log.error(msg, newException);
PluginLifecycleEventException wrapper = new PluginLifecycleEventException(Executions.getCurrent(), msg,
previousException == null ? newException : previousException);
wrapper.setStackTrace(newException.getStackTrace());
return wrapper;
}
/**
* Initializes a plugin, if not already done. This loads the plugin's principal zul page,
* attaches any event listeners, and sends a load event to subscribers.
*/
public void load() {
if (!initialized && definition != null) {
try {
initialized = true;
if (getFirstChild() == null) {
ZKUtil.loadZulPage(definition.getUrl(), this);
}
} catch (Throwable e) {
ZKUtil.detachChildren(this);
throw createChainedException("Initialize", e, null);
}
findListeners(this);
executeAction(PluginAction.LOAD, true);
}
}
/**
* Search the plugin's component tree for components (or their controllers) implementing the
* IPluginEvent interface. Those that are found are registered as listeners.
*
* @param cmpt Component to search
*/
private void findListeners(Component cmpt) {
for (Component child : cmpt.getChildren()) {
tryRegisterListener(child, true);
tryRegisterListener(FrameworkController.getController(child), true);
findListeners(child);
}
}
/**
* Adds the specified component to the toolbar container. The component is registered to this
* container and will visible only when the container is active.
*
* @param component Component to add.
*/
public void addToolbarComponent(Component component) {
if (tbarContainer == null) {
tbarContainer = new ToolbarContainer();
shell.addToolbarComponent(tbarContainer);
registerComponent(tbarContainer);
}
tbarContainer.appendChild(component);
}
/**
* Register a component with the container. The container will control the visibility of the
* component according to when it is active/inactive.
*
* @param component Component to register.
*/
public void registerComponent(Component component) {
if (registeredComponents == null) {
registeredComponents = new ArrayList<>();
}
registeredComponents.add(component);
component.setAttribute(Constants.ATTR_CONTAINER, this);
component.setAttribute(Constants.ATTR_VISIBLE, component.isVisible());
component.setVisible(isVisible());
}
/**
* Allows auto-wire to work even if component is not a child of the container.
*
* @param id Component id.
* @param component Component to be registered.
*/
public void registerId(String id, Component component) {
if (!StringUtils.isEmpty(id) && !hasAttribute(id)) {
setAttribute(id, component);
}
}
/**
* Registers an action element. Action elements implement the Disable interface and are
* automatically enabled/disabled when the owning container is enabled/disabled.
*
* @param actionElement A component implementing the Disable interface.
*/
public void registerAction(Disable actionElement) {
if (registeredActions == null) {
registeredActions = new ArrayList<>();
}
registeredActions.add(actionElement);
actionElement.setDisabled(isDisabled());
}
/**
* Registers a listener for the IPluginEvent callback event. If the listener has already been
* registered, the request is ignored.
*
* @param listener Listener to be registered.
*/
public void registerListener(IPluginEvent listener) {
if (pluginEventListeners1 == null) {
pluginEventListeners1 = new ArrayList<>();
}
if (!pluginEventListeners1.contains(listener)) {
pluginEventListeners1.add(listener);
}
}
/**
* Registers a listener for the IPluginEventListener callback event. If the listener has already
* been registered, the request is ignored.
*
* @param listener Listener to be registered.
*/
public void registerListener(IPluginEventListener listener) {
if (pluginEventListeners2 == null) {
pluginEventListeners2 = new ArrayList<>();
}
if (!pluginEventListeners2.contains(listener)) {
pluginEventListeners2.add(listener);
listener.onPluginEvent(new PluginEvent(this, PluginAction.SUBSCRIBE));
}
}
/**
* Unregisters a listener for the IPluginEvent callback event.
*
* @param listener Listener to be unregistered.
*/
public void unregisterListener(IPluginEvent listener) {
if (pluginEventListeners1 != null) {
pluginEventListeners1.remove(listener);
}
}
/**
* Unregisters a listener for the IPluginEvent callback event.
*
* @param listener Listener to be unregistered.
*/
public void unregisterListener(IPluginEventListener listener) {
if (pluginEventListeners2 != null && pluginEventListeners2.contains(listener)) {
pluginEventListeners2.remove(listener);
listener.onPluginEvent(new PluginEvent(this, PluginAction.UNSUBSCRIBE));
}
}
/**
* Attempts to register or unregister an object as an event listener.
*
* @param object Object to register/unregister.
* @param register If true, we are attempting to register. If false, unregister.
* @return True if operation was successful. False if the object supports none of the recognized
* event listeners.
*/
public boolean tryRegisterListener(Object object, boolean register) {
boolean success = false;
if (object instanceof IPluginEvent) {
if (register) {
registerListener((IPluginEvent) object);
} else {
unregisterListener((IPluginEvent) object);
}
success = true;
}
if (object instanceof IPluginEventListener) {
if (register) {
registerListener((IPluginEventListener) object);
} else {
unregisterListener((IPluginEventListener) object);
}
success = true;
}
return success;
}
/**
* Registers one or more named properties to the container. Using this, a plugin can expose
* properties for serialization and deserialization.
*
* @param instance The object instance holding the property accessors. If null, any existing
* registration will be removed.
* @param propertyNames One or more property names to register.
*/
public void registerProperties(Object instance, String... propertyNames) {
for (String propertyName : propertyNames) {
registerProperty(instance, propertyName, true);
}
}
/**
* Registers a named property to the container. Using this, a plugin can expose a property for
* serialization and deserialization.
*
* @param instance The object instance holding the property accessors. If null, any existing
* registration will be removed.
* @param propertyName Name of property to register.
* @param override If the property is already registered to a non-proxy, the previous
* registration will be replaced if this is true; otherwise the request is ignored.
*/
public void registerProperty(Object instance, String propertyName, boolean override) {
if (registeredProperties == null) {
registeredProperties = new HashMap<>();
}
if (instance == null) {
registeredProperties.remove(propertyName);
} else {
Object oldInstance = registeredProperties.get(propertyName);
PropertyProxy proxy = oldInstance instanceof PropertyProxy ? (PropertyProxy) oldInstance : null;
if (!override && oldInstance != null && proxy == null) {
return;
}
registeredProperties.put(propertyName, instance);
// If previous registrant was a property proxy, transfer its value to new registrant.
if (proxy != null) {
try {
proxy.propInfo.setPropertyValue(instance, proxy.value);
} catch (Exception e) {
throw createChainedException("Register Property", e, null);
}
}
}
}
/**
* Registers a helper bean with this container.
*
* @param beanId The bean's id.
* @param isRequired If true and the bean is not found, an exception is raised.
*/
public void registerBean(String beanId, boolean isRequired) {
if (beanId == null || beanId.isEmpty()) {
return;
}
Object bean = SpringUtil.getBean(beanId);
if (bean == null && isRequired) {
throw new PluginLifecycleEventException(Executions.getCurrent(), "Required bean resouce not found: " + beanId);
}
Object oldBean = getAssociatedBean(beanId);
if (bean == oldBean) {
return;
}
if (registeredBeans == null) {
registeredBeans = new HashMap<>();
}
tryRegisterListener(oldBean, false);
if (bean == null) {
registeredBeans.remove(beanId);
} else {
registeredBeans.put(beanId, bean);
tryRegisterListener(bean, true);
}
}
/**
* Returns a bean that has been associated (via registerBean) with this plugin.
*
* @param beanId The id of the bean.
* @return The bean instance, or null if not found.
*/
public Object getAssociatedBean(String beanId) {
return registeredBeans == null ? null : registeredBeans.get(beanId);
}
/**
* Returns the value for a registered property.
*
* @param propInfo Property info.
* @return The property value.
* @throws Exception Unspecified exception.
*/
public Object getPropertyValue(PropertyInfo propInfo) throws Exception {
Object obj = registeredProperties == null ? null : registeredProperties.get(propInfo.getId());
if (obj instanceof PropertyProxy) {
Object value = ((PropertyProxy) obj).value;
return value instanceof String ? propInfo.getPropertyType().getSerializer().deserialize((String) value) : value;
} else {
return obj == null ? null : propInfo.getPropertyValue(obj);
}
}
/**
* Sets a value for a registered property.
*
* @param propInfo Property info.
* @param value The value to set.
* @throws Exception Unspecified exception.
*/
public void setPropertyValue(PropertyInfo propInfo, Object value) throws Exception {
String propId = propInfo.getId();
Object obj = registeredProperties == null ? null : registeredProperties.get(propId);
if (obj == null) {
obj = new PropertyProxy(propInfo, value);
registerProperties(obj, propId);
} else if (obj instanceof PropertyProxy) {
((PropertyProxy) obj).value = value;
} else {
propInfo.setPropertyValue(obj, value);
}
}
/**
* Return the definition associated with this plugin.
*
* @return The associated plugin definition.
*/
public PluginDefinition getPluginDefinition() {
return definition;
}
/**
* Sets the plugin definition the container will use to instantiate the plugin. If there is a
* status bean associated with the plugin, it is registered with the container at this time. If
* there are style sheet resources associated with the plugin, they will be added to the
* container at this time.
*
* @param definition The plugin definition.
*/
public void setPluginDefinition(PluginDefinition definition) {
this.definition = definition;
if (definition == null) {
return;
}
setSclass("cwf-plugin-" + definition.getId());
shell.registerPlugin(this);
}
/**
* Enables/disables the container and all registered action elements.
*
* @param disabled Disable status.
*/
public void setDisabled(boolean disabled) {
this.disabled = disabled;
disableActions(disabled);
}
/**
* Enables/disables registered actions elements. Note that if the container is disabled, the
* action elements will not be enabled by this call. It may be used, however, to temporarily
* disable action elements that would otherwise be enabled.
*
* @param disable The disable status.
*/
public void disableActions(boolean disable) {
if (registeredActions != null) {
for (Disable registeredAction : registeredActions) {
registeredAction.setDisabled(disable || disabled);
}
}
}
/**
* Returns the disable status of the container.
*
* @return True if this plugin is disabled.
*/
public boolean isDisabled() {
return disabled;
}
/**
* Temporarily disables setBusy function.
*
* @param disable If true, disable setBusy function. If false, enables the function and
* processes any pending busy operation.
*/
private void disableBusy(boolean disable) {
busyDisabled = disable;
busyPending |= disable;
checkBusy();
}
/**
* Processes any pending busy operation if enabled.
*/
private void checkBusy() {
if (!busyDisabled && busyPending) {
setBusy(busyMessage);
}
}
/**
* If message is not null, disables the plugin and displays the busy message. If message is
* null, removes any previous message and returns the plugin to its previous state.
*
* @param message The message to display, or null to clear previous message.
*/
public void setBusy(String message) {
busyMessage = message = StrUtil.formatMessage(message);
if (busyDisabled) {
busyPending = true;
} else if (message != null) {
disableActions(true);
Clients.showBusy(this, message);
busyPending = !isVisible();
} else {
disableActions(false);
Clients.clearBusy(this);
busyPending = false;
}
}
/**
* Sets design mode for the container.
*
* @param designMode If true, associated actions and busy mask are disabled.
*/
public void setDesignMode(boolean designMode) {
disableActions(designMode);
disableBusy(designMode);
}
/**
* Returns the shell instance that hosts this container.
*
* @return The shell instance.
*/
public CareWebShell getShell() {
return shell;
}
/**
* Returns the color (as an HTML-formatted RGB string) for this element.
*
* @return An HTML-formatted color specification (e.g., #0F134E). May be null.
*/
public String getColor() {
return color;
}
/**
* Sets the container's background color.
*
* @param value A correctly formatted HTML color specification.
*/
public void setColor(String value) {
color = value;
ZKUtil.updateStyle(this, "background-color", color);
}
}