/* * Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved. * * This file is part of the Jspresso framework. * * Jspresso is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Jspresso 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Jspresso. If not, see <http://www.gnu.org/licenses/>. */ package org.jspresso.framework.application.model; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.jspresso.framework.action.IAction; import org.jspresso.framework.application.backend.IBackendController; import org.jspresso.framework.security.ISecurable; import org.jspresso.framework.security.ISecurityHandler; import org.jspresso.framework.util.automation.IPermIdSource; import org.jspresso.framework.util.bean.AbstractPropertyChangeCapable; import org.jspresso.framework.util.gui.Icon; import org.jspresso.framework.util.lang.ObjectUtils; import org.jspresso.framework.util.lang.StringUtils; import org.jspresso.framework.view.descriptor.IViewDescriptor; import org.jspresso.framework.view.descriptor.IViewDescriptorProvider; /** * A module is an entry point in the application. Modules are organized in * bi-directional, parent-children hierarchy. As such, they can be viewed (and * they are materialized in the UI) as trees. Modules can be (re)organized * dynamically by changing their parent-children relationship and their owning * workspace UI will reflect the change seamlessly, as with any Jspresso model * (in fact workspaces and modules are regular beans that are used as model in * standard Jspresso views). * <p/> * Modules, among other features, are capable of providing a view to be * installed in the UI wen they are selected. This makes Jspresso applications * really modular and their architecture flexible enough to embed and run a * large variety of different module types. * <p/> * A module can also be as simple as a grouping structure for other modules * (intermediary nodes). * * @author Vincent Vandenschrick */ public class Module extends AbstractPropertyChangeCapable implements IViewDescriptorProvider, ISecurable, IPermIdSource { /** * {@code DESCRIPTION} is "description". */ public static final String DESCRIPTION = "description"; /** * {@code I18N_DESCRIPTION} is "i18nDescription". */ public static final String I18N_DESCRIPTION = "i18nDescription"; /** * {@code I18N_NAME} is "i18nName". */ public static final String I18N_NAME = "i18nName"; /** * {@code NAME} is "name". */ public static final String NAME = "name"; /** * {@code PARENT} is "parent". */ public static final String PARENT = "parent"; /** * {@code SUB_MODULES} is "subModules". */ public static final String SUB_MODULES = "subModules"; /** * {@code DIRTY} is "dirty". */ public static final String DIRTY = "dirty"; /** * {@code HEADER_DESCRIPTION} is "headerDescription". */ public static final String HEADER_DESCRIPTION = "headerDescription"; /** * {@code I18N_HEADER_DESCRIPTION} is "i18nHeaderDescription". */ public static final String I18N_HEADER_DESCRIPTION = "i18nHeaderDescription"; private String description; private String i18nDescription; private String headerDescription; private String i18nHeaderDescription; private boolean dirty; private IAction entryAction; private IAction exitAction; private Collection<String> grantedRoles; private String i18nName; private Icon icon; private String name; private Module parent; private IViewDescriptor projectedViewDescriptor; private boolean started; private IAction startupAction; private ISecurityHandler securityHandler; private List<Module> subModules; private Collection<Module> stickySubModules; private String permId; /** * Constructs a new {@code Module} instance. */ public Module() { started = false; dirty = false; } /** * Equality based on projected object. * <p/> * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (!(obj instanceof Module)) { return false; } if (this == obj) { return true; } Module rhs = (Module) obj; EqualsBuilder equalsBuilder = new EqualsBuilder(); if (name != null) { equalsBuilder.append(name, rhs.name); } if (i18nName != null) { equalsBuilder.append(i18nName, rhs.i18nName); } if (projectedViewDescriptor != null) { equalsBuilder.append(projectedViewDescriptor, rhs.projectedViewDescriptor); } return equalsBuilder.isEquals(); } /** * Adds a child module. * * @param child * the child module to add. It will fire a "subModules" * property change event. * @return {@code true} if the module was successfully added. */ public boolean addSubModule(Module child) { if (subModules == null) { subModules = new ArrayList<>(); } List<Module> oldValue = new ArrayList<>(getSubModules()); if (subModules.add(child)) { updateParentsAndFireSubModulesChanged(oldValue, getSubModules()); return true; } return false; } /** * Adds a modules module collection. It will fire a "subModules" * property change event. * * @param children * the modules modules to add. * @return {@code true} if the modules module collection was successfully * added. */ public boolean addSubModules(Collection<? extends Module> children) { if (subModules == null) { subModules = new ArrayList<>(); } List<Module> oldValue = new ArrayList<>(getSubModules()); if (subModules.addAll(children)) { updateParentsAndFireSubModulesChanged(oldValue, getSubModules()); return true; } return false; } /** * Gets the module's description. It may serve for the module's view. * * @return the module's description. */ public String getDescription() { return description; } /** * Gets the entryAction. * * @return the entryAction. */ public IAction getEntryAction() { return entryAction; } /** * Gets the exitAction. * * @return the exitAction. */ public IAction getExitAction() { return exitAction; } /** * Gets the grantedRoles. * * @return the grantedRoles. */ @Override public Collection<String> getGrantedRoles() { if (grantedRoles == null && projectedViewDescriptor != null) { return projectedViewDescriptor.getGrantedRoles(); } return grantedRoles; } /** * Gets the i18nDescription. * * @return the i18nDescription. */ public String getI18nDescription() { if (i18nDescription != null) { return i18nDescription; } return getDescription(); } /** * Gets the i18nName. * * @return the i18nName. */ public String getI18nName() { String dirtyMarker = ""; if (isDirty()) { dirtyMarker = "(*)"; } if (i18nName != null) { return dirtyMarker + i18nName; } return getName(); } /** * Gets the icon. * * @return the icon. */ public Icon getIcon() { return icon; } /** * Gets the module's name. It may serve for the module's view. * * @return the module's name. */ public String getName() { return name; } /** * Gets the module's parent module. * * @return the parent module or null if none. */ public Module getParent() { return parent; } /** * Gets the projectedViewDescriptor. * * @return the projectedViewDescriptor. */ public IViewDescriptor getProjectedViewDescriptor() { if (projectedViewDescriptor != null && projectedViewDescriptor.getPermId() == null) { projectedViewDescriptor.setPermId(getPermId() + ".projectedView"); } return projectedViewDescriptor; } /** * Gets the startupAction. * * @return the startupAction. */ public IAction getStartupAction() { return startupAction; } /** * Gets the modules sub-modules. * * @return the list of modules modules. */ public List<Module> getSubModules() { return getSubModules(false); } /** * Gets the modules sub-modules. * * @param bypassSecurity * bypasses security restrictions imposed to the user. * @return the list of modules modules. */ public List<Module> getSubModules(boolean bypassSecurity) { if (subModules == null) { return null; } ISecurityHandler sh = getSecurityHandler(); if (sh != null) { for (Iterator<Module> ite = subModules.iterator(); ite.hasNext(); ) { Module nextModule = ite.next(); if (!bypassSecurity && !sh.isAccessGranted(nextModule)) { try { sh.pushToSecurityContext(nextModule); ite.remove(); } finally { sh.restoreLastSecurityContextSnapshot(); } } } } return subModules; } /** * Returns unmodified projected view descriptor. * <p/> * {@inheritDoc} */ @Override public IViewDescriptor getViewDescriptor() { return getProjectedViewDescriptor(); } /** * Hash code based on name. * <p/> * {@inheritDoc} */ @Override public int hashCode() { return new HashCodeBuilder(23, 57).append(name).toHashCode(); } /** * Gets the dirty. * * @return the dirty. */ public boolean isDirty() { return dirty; } /** * Gets the started. * * @return the started. */ public boolean isStarted() { return started; } /** * Removes a child module. It will fire a "subModules" property * change event. * * @param module * the child module to remove. * @return {@code true} if the module was successfully removed. */ public boolean removeSubModule(Module module) { if (subModules != null) { List<Module> oldValue = new ArrayList<>(getSubModules()); if (subModules.remove(module)) { updateParentsAndFireSubModulesChanged(oldValue, getSubModules()); return true; } return false; } return false; } /** * Removes a modules module collection. It will fire a "subModules" * property change event. * * @param children * the modules modules to remove. * @return {@code true} if the modules module collection was successfully * removed. */ public boolean removeSubModules(Collection<Module> children) { if (subModules != null) { List<Module> oldValue = new ArrayList<>(getSubModules()); if (subModules.removeAll(children)) { updateParentsAndFireSubModulesChanged(oldValue, getSubModules()); return true; } return false; } return false; } /** * Configures the key used to translate actual internationalized module * description. The resulting translation will generally be leveraged as a * toolTip on the UI side but its use may be extended for online help. * * @param description * the module's description. */ public void setDescription(String description) { if (ObjectUtils.equals(this.description, description)) { return; } String oldValue = getDescription(); this.description = description; firePropertyChange(DESCRIPTION, oldValue, getDescription()); if (this.i18nDescription == null) { setI18nDescription(name); } } /** * Sets the dirty. * * @param dirty * the dirty to set. * @internal */ protected void setDirty(boolean dirty) { boolean oldDirty = isDirty(); String oldI18nName = getI18nName(); this.dirty = dirty; firePropertyChange(DIRTY, oldDirty, isDirty()); firePropertyChange(I18N_NAME, oldI18nName, getI18nName()); } /** * Configures an action to be executed every time the module becomes the * current selected module (either through a user explicit navigation or a * programmatic selection). The action will execute in the context of the * current workspace, this module being the current selected module. * * @param entryAction * the entryAction to set. */ public void setEntryAction(IAction entryAction) { this.entryAction = entryAction; } /** * Configures an action to be executed every time the module becomes * unselected (either through a user explicit navigation or a programmatic * deselection). The action will execute in the context of the current * workspace, this module being the current selected module (i.e. the action * occurs before the module is actually left). * * @param exitAction * the exitAction to set. */ public void setExitAction(IAction exitAction) { this.exitAction = exitAction; } /** * Assigns the roles that are authorized to start this module. It supports * "<b>!</b>" prefix to negate the role(s). Whenever the user is not * granted sufficient privileges, the module is simply not installed in the * workspace. Setting the collection of granted roles to {@code null} * (default value) disables role based authorization on this module. * <p/> * Some specific modules that are component/entity model based i.e. * {@code Bean(Collection)Module} also inherit their authorizations from * their model. * * @param grantedRoles * the grantedRoles to set. */ public void setGrantedRoles(Collection<String> grantedRoles) { this.grantedRoles = StringUtils.ensureSpaceFree(grantedRoles); } /** * Stores the internationalized workspace description for use in the UI as * toolTip for instance. * * @param i18nDescription * the i18nDescription to set. * @internal */ public void setI18nDescription(String i18nDescription) { if (ObjectUtils.equals(this.i18nDescription, i18nDescription)) { return; } String oldValue = getI18nDescription(); this.i18nDescription = i18nDescription; firePropertyChange(I18N_DESCRIPTION, oldValue, getI18nDescription()); if (this.description == null) { setDescription(i18nDescription); } } /** * Stores the internationalized workspace name for use in the UI as workspace * label. * * @param i18nName * the i18nName to set. * @internal */ public void setI18nName(String i18nName) { if (ObjectUtils.equals(this.i18nName, i18nName)) { return; } String oldValue = getI18nName(); this.i18nName = i18nName; firePropertyChange(I18N_NAME, oldValue, getI18nName()); // Having a static name prevents changing the i18nName based on module // object for bean modules. // So never assign it programmatically. // if (this.name == null) { // setName(i18nName); // } } /** * Sets the icon image URL of this descriptor. Supported URL protocols include * : * <ul> * <li>all JVM supported protocols</li> * <li>the <b>jar:/</b> pseudo URL protocol</li> * <li>the <b>classpath:/</b> pseudo URL protocol</li> * </ul> * * @param iconImageURL * the iconImageURL to set. */ public void setIconImageURL(String iconImageURL) { if (icon == null) { icon = new Icon(); } icon.setIconImageURL(iconImageURL); } /** * Sets the icon preferred width. * * @param iconPreferredWidth * the iconPreferredWidth to set. */ public void setIconPreferredWidth(int iconPreferredWidth) { if (icon == null) { icon = new Icon(); } icon.setWidth(iconPreferredWidth); } /** * Sets the icon preferred width. * * @param iconPreferredHeight * the iconPreferredHeight to set. */ public void setIconPreferredHeight(int iconPreferredHeight) { if (icon == null) { icon = new Icon(); } icon.setHeight(iconPreferredHeight); } /** * Sets the icon. * * @param icon * the icon to set. */ public void setIcon(Icon icon) { this.icon = icon; } /** * Configures the key used to translate actual internationalized module name. * The resulting translation will be leveraged as the module label on the UI * side. * * @param name * the module's name. */ public void setName(String name) { if (ObjectUtils.equals(this.name, name)) { return; } String oldValue = getName(); this.name = name; firePropertyChange(NAME, oldValue, getName()); if (this.i18nName == null) { setI18nName(name); } } /** * Assigns the parent module and potentially move itself out of previous * parent children. It will fire a "parent" property change event. * * @param parent * the parent module to set or null if none. * @internal */ public void setParent(Module parent) { if (ObjectUtils.equals(this.parent, parent)) { return; } Module oldParent = getParent(); if (oldParent != null) { oldParent.removeSubModule(this); } this.parent = parent; if (parent != null && (parent.getSubModules() == null || !parent.getSubModules().contains(this))) { parent.addSubModule(this); } firePropertyChange(PARENT, oldParent, parent); } /** * Configures the view descriptor used to construct the view that will be * displayed when this module is selected. * * @param projectedViewDescriptor * the projectedViewDescriptor to set. */ public void setProjectedViewDescriptor(IViewDescriptor projectedViewDescriptor) { this.projectedViewDescriptor = projectedViewDescriptor; } /** * Sets the started. * * @param started * the started to set. * @internal */ public void setStarted(boolean started) { // Take a snapshot of the existing sub-modules if (!isStarted() && started && subModules != null) { stickySubModules = new HashSet<>(); for (Module subModule : subModules) { if (isSubModuleSticky(subModule)) { stickySubModules.add(subModule); } } } this.started = started; } /** * Configures an action to be executed the first time the module is * "started" by the user. The action will execute in the context of * the current workspace, this module being the current selected module. It * will help initializing module values, notify user, .... * * @param startupAction * the startupAction to set. */ public void setStartupAction(IAction startupAction) { this.startupAction = startupAction; } /** * Configures the security handler used to secure this module. * * @param securityHandler * the security handler. * @internal */ public void setSecurityHandler(ISecurityHandler securityHandler) { this.securityHandler = securityHandler; } /** * Installs a list of module(s) as sub-modules of this one. It will fire a * "subModules" property change event. * * @param children * the modules modules to set. * @internal */ public void setSubModules(List<Module> children) { List<Module> oldValue = null; if (getSubModules() != null) { oldValue = new ArrayList<>(getSubModules()); } this.subModules = children; updateParentsAndFireSubModulesChanged(oldValue, getSubModules()); } /** * based on name. * <p/> * {@inheritDoc} */ @Override public String toString() { if (getI18nName() != null) { return getI18nName(); } return ""; } /** * This method will set the parent module to the new modules modules and * remove the parent of the old removed modules modules. It will fire the * "subModules" property change event. * * @param oldChildren * the old modules collection property. * @param newChildren * the new modules collection property. */ protected void updateParentsAndFireSubModulesChanged(List<Module> oldChildren, List<Module> newChildren) { if (oldChildren != null) { for (Module oldChild : oldChildren) { if (newChildren == null || !newChildren.contains(oldChild)) { oldChild.setParent(null); } } } if (newChildren != null) { for (Module newChild : newChildren) { if (oldChildren == null || !oldChildren.contains(newChild)) { newChild.setParent(this); } } } firePropertyChange(SUB_MODULES, oldChildren, newChildren); } private ISecurityHandler getSecurityHandler() { if (securityHandler != null) { return securityHandler; } if (getParent() != null) { return getParent().getSecurityHandler(); } return null; } /** * Gets the permId. * * @return the permId. */ @Override public String getPermId() { if (permId != null) { return permId; } return getName(); } /** * Sets the permanent identifier to this application element. Permanent * identifiers are used by different framework parts, like dynamic security or * record/replay controllers to uniquely identify an application element. * Permanent identifiers are generated by the SJS build based on the element * id but must be explicitly set if Spring XML is used. * * @param permId * the permId to set. */ @Override public void setPermId(String permId) { this.permId = permId; } /** * {@inheritDoc} */ @Override public Module clone() { Module clone = (Module) super.clone(); clone.parent = null; List<Module> subModulesClones = new ArrayList<>(); if (subModules != null) { for (Module subModule : subModules) { Module subModuleClone = subModule.clone(); subModuleClone.parent = clone; subModulesClones.add(subModuleClone); } } clone.subModules = subModulesClones; return clone; } /** * Compute dirtiness in depth. * * @param backendController * the backend controller * @return true if the module or one of its sub-module is dirty */ public final boolean refreshDirtinessInDepth(IBackendController backendController) { boolean depthDirtyness = false; if (getSubModules() != null) { for (Module subModule : getSubModules()) { if (subModule.refreshDirtinessInDepth(backendController)) { depthDirtyness = true; } } } depthDirtyness = depthDirtyness || isLocallyDirty(backendController); setDirty(depthDirtyness); return depthDirtyness; } /** * Is this module locally dirty. * * @param backendController * the backend controller * @return {@code true} if this module is dirty itself (without considering its children) */ protected boolean isLocallyDirty(IBackendController backendController) { return false; } /** * Is sub module sticky and should not be removed when the module is restarted. * * @param subModule the sub module * @return the boolean */ public boolean isSubModuleSticky(Module subModule) { if (isStarted()) { return stickySubModules != null && stickySubModules.contains(subModule); } return true; } /** * Gets header description. * * @return the header description */ public String getHeaderDescription() { return headerDescription; } /** * Configures the key used to translate actual internationalized module * header description. The resulting translation will generally be leveraged as a * text header on the UI side. * * @param headerDescription * the header description */ public void setHeaderDescription(String headerDescription) { this.headerDescription = headerDescription; } /** * Gets i 18 n header description. * * @return the i 18 n header description */ public String getI18nHeaderDescription() { if (i18nHeaderDescription != null) { return i18nHeaderDescription; } return getHeaderDescription(); } /** * Sets i 18 n header description. * * @param i18nHeaderDescription * the i 18 n header description */ public void setI18nHeaderDescription(String i18nHeaderDescription) { this.i18nHeaderDescription = i18nHeaderDescription; } }