The topic of this post is not new. You can find some discussions and ideas in the web. I would only mention here two links. The first one is the article "How-to: Modular Java EE Applications with CDI and PrettyFaces" in the ocpsoft's blog. The second one "Modular Web Apps with JSF2" was presented in the JBoss' wiki. The idea is to create JAR files containing single web applications and to supply them with a main WAR file. The WAR file bundles JARs during build process, e.g. via Maven dependencies. That means, JARs are located in the WAR under WEB-INF/lib/. XHTML files in JARs are placed below /META-INF/resources/ and will be fetched automatically by JSF 2. They are available to JSF as if they were in the /webapp/resources/ folder. You can e.g. include facelets from JARs with a quite common ui:include. This works like a charm. To be able to pick up generic informations about every JSF module at runtime, we also need empty CDI's beans.xml in JARs files. They are located as usually below the META-INF folder.
Now let's start with the coding. But first, let's define the project's structure. You can find a complete implemented example on the GitHub. This is just a proof of concept for a lightweight JSF 2 portal-like implementation with demo web apps (written with JSF 2.2). There are 5 sub-projects
jsftoolkit-jar Base framework providing interfaces and utilities for modular JSF applications.
modA-jar First web application (module A) which depends on jsftoolkit-jar.
modB-jar Second web application (module B) which depends on jsftoolkit-jar.
portal-jar Java part of the portal-like software. It also depends on jsftoolkit-jar.
portal-war Web part of the portal-like software. It aggregates all artefacts and is a deployable WAR.
The base framework (jsftoolkit-jar) has interfaces which should be implemented by every single module. The most important are
/** * Interface for modular JSF applications. This interface should be implemented by every module (JSF app.) * to allow a seamless integration into a "portal" software. */ public interface ModuleDescription { /** * Provides a human readable name of the module. * * @return String name of the module */ String getName(); /** * Provides a description of the module. * * @return String description */ String getDescription(); /** * Provides a module specific prefix. This is a folder below the context where all web pages and * resources are located. * * @return String prefix */ String getPrefix(); /** * Provides a name for a logo image, e.g. "images/logo.png" (used in h:graphicImage). * * @return String logo name */ String getLogoName(); /** * Provides a start (home) URL to be navigated for the module. * * @return String URL */ String getUrl(); } /** * Any JSF app. implementing this interface can participate in an unified message handling * when all keys and messages are merged to a map and available via "msgs" EL, e.g. as #{msgs['mykey']}. */ public interface MessagesProvider { /** * Returns all mesages (key, text) to the module this interface is implemented for. * * @param locale current Locale or null * @return Map with message keys and message text. */ Map<String, String> getMessages(Locale locale); }Possible implementations for the module A look like as follows:
/** * Module specific implementation of the {@link ModuleDescription}. */ @ApplicationScoped @Named public class ModADescription implements ModuleDescription, Serializable { @Inject private MessagesProxy msgs; @Override public String getName() { return msgs.get("a.modName"); } @Override public String getDescription() { return msgs.get("a.modDesc"); } @Override public String getPrefix() { return "moda"; } @Override public String getLogoName() { return "images/logo.png"; } @Override public String getUrl() { return "/moda/views/hello.jsf"; } } /** * Module specific implementation of the {@link MessagesProvider}. */ @ApplicationScoped @Named public class ModAMessages implements MessagesProvider, Serializable { @Override public Map<String, String> getMessages(Locale locale) { return MessageUtils.getMessages(locale, "modA"); } }The prefix of this module is moda. That means, web pages and resources are located under the folder META-INF/resources/moda/. That allows to avoid path collisions (identical paths) across all single web applications. The utility class MessageUtils (from the jsftoolkit-jar) is not exposed here. I will only show the class MessagesProxy. The class MessagesProxy is an application scoped bean giving an access to all available messages in a modular JSF web application. It can be used in Java as well as in XHTML because it implements the Map interface. All found available implementations of the MessagesProvider interface are injected by CDI automatically at runtime. We make use of Instance<MessagesProvider>.
@ApplicationScoped @Named(value = "msgs") public class MessagesProxy implements Map<String, String>, Serializable { @Inject private UserSettingsData userSettingsData; @Any @Inject private Instance<MessagesProvider> messagesProviders; /** all cached locale specific messages */ private Map<Locale, Map<String, String>> msgs = new ConcurrentHashMap<Locale, Map<String, String>>(); @Override public String get(Object key) { if (key == null) { return null; } Locale locale = userSettingsData.getLocale(); Map<String, String> messages = msgs.get(locale); if (messages == null) { // no messages to current locale are available yet messages = new HashMap<String, String>(); msgs.put(locale, messages); // load messages from JSF impl. first messages.putAll(MessageUtils.getMessages(locale, MessageUtils.FACES_MESSAGES)); // load messages from providers in JARs for (MessagesProvider messagesProvider : messagesProviders) { messages.putAll(messagesProvider.getMessages(locale)); } } return messages.get(key); } public String getText(String key) { return this.get(key); } public String getText(String key, Object... params) { String text = this.get(key); if ((text != null) && (params != null)) { text = MessageFormat.format(text, params); } return text; } public FacesMessage getMessage(FacesMessage.Severity severity, String key, Object... params) { String summary = this.get(key); String detail = this.get(key + "_detail"); if ((summary != null) && (params != null)) { summary = MessageFormat.format(summary, params); } if ((detail != null) && (params != null)) { detail = MessageFormat.format(detail, params); } if (summary != null) { return new FacesMessage(severity, summary, ((detail != null) ? detail : StringUtils.EMPTY)); } return new FacesMessage(severity, "???" + key + "???", ((detail != null) ? detail : StringUtils.EMPTY)); } ///////////////////////////////////////////////////////// // java.util.Map interface ///////////////////////////////////////////////////////// public int size() { throw new UnsupportedOperationException(); } // other methods ... }Well. But where the instances of the ModuleDescription are picked up? The logic is in the portal-jar. I use the same mechanism with CDI Instance. CDI will find out all available implementations of the ModuleDescription for us.
/** * Collects all available JSF modules. */ @ApplicationScoped @Named public class PortalModulesFinder implements ModulesFinder { @Any @Inject private Instance<ModuleDescription> moduleDescriptions; @Inject private MessagesProxy msgs; private List<FluidGridItem> modules; @Override public List<FluidGridItem> getModules() { if (modules != null) { return modules; } modules = new ArrayList<FluidGridItem>(); for (ModuleDescription moduleDescription : moduleDescriptions) { modules.add(new FluidGridItem(moduleDescription)); } // sort modules by names alphabetically Collections.sort(modules, ModuleDescriptionComparator.getInstance()); return modules; } }We are equipped now for creating dynamic tiles in UI which represent entry points to the corresponding web modules.
<pe:fluidGrid id="fluidGrid" value="#{portalModulesFinder.modules}" var="modDesc" fitWidth="true" hasImages="true"> <pe:fluidGridItem styleClass="ui-widget-header"> <h:panelGrid columns="2" styleClass="modGridEntry" columnClasses="modLogo,modTxt"> <p:commandLink process="@this" action="#{navigationContext.goToPortlet(modDesc)}"> <h:graphicImage library="#{modDesc.prefix}" name="#{modDesc.logoName}"/> </p:commandLink> <h:panelGroup> <p:commandLink process="@this" action="#{navigationContext.goToPortlet(modDesc)}"> <h:outputText value="#{modDesc.name}" styleClass="linkToPortlet"/> </p:commandLink> <p/> <h:outputText value="#{modDesc.description}"/> </h:panelGroup> </h:panelGrid> </pe:fluidGridItem> </pe:fluidGrid>Tiles were created by the component pe:fluidGrid from PrimeFaces Extensions. They are responsive, means they get rearranged when resizing the browser window. The next picture demonstrates how the portal web app looks like after the starting up. It shows two modular demo apps found in the classpath. Every modular web app is displayed as a tile containing a logo, name and short description. Logos and names are clickable. A click redirects to the corresponsing single web app.
As you can see, on the portal's homepage you can switch the current language and the theme. The second picture shows what happens if the user clicks on the module A. The web app for the module A is shown. You can see the Back to Portal button, so a back navigation to the portal's homepage is possible.
Two notes at the end:
- It is possible to run every module as a standalone web application. We can check at runtime (again by means of CDI) if the module is within the "portal" or not and use different master templates. A hint is here.
- I didn't implement a login screen, but there is no issue with single sign-on because we only have one big application (one WAR). Everything is delivered there.