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