From 262869583bdba44a42b33a6b616cfe968481f78d Mon Sep 17 00:00:00 2001 From: Henri Sara Date: Thu, 26 Nov 2009 09:47:35 +0000 Subject: [PATCH] #3117 Minor cleanup and fixes in Portlet 2.0 support svn changeset:10062/svn branch:6.2 --- .../gwt/client/ApplicationConfiguration.java | 16 ++- .../gwt/client/ApplicationConnection.java | 90 +++++++++-------- .../server/AbstractApplicationPortlet.java | 99 ++++++++++--------- .../server/AbstractApplicationServlet.java | 4 +- .../gwt/server/ApplicationPortlet.java | 12 +-- .../vaadin/terminal/gwt/server/Constants.java | 11 ++- .../server/PortletApplicationContext2.java | 19 ++-- 7 files changed, 134 insertions(+), 117 deletions(-) diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java b/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java index f5df828a2c..8a496d4631 100644 --- a/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java @@ -35,15 +35,21 @@ public class ApplicationConfiguration { public boolean usePortletURLs() { return usePortletURLs; } - + public String getPortletUidlURLBase() { return portletUidlURLBase; } - + public String getRootPanelId() { return id; } + /** + * Gets the application base URI. Using this other than as the download + * action URI can cause problems in Portlet 2.0 deployments. + * + * @return application base URI + */ public String getApplicationUri() { return appUri; } @@ -120,14 +126,14 @@ public class ApplicationConfiguration { } else { $wnd.alert("Vaadin app failed to initialize: " + this.id); } - + }-*/; /** * Inits the ApplicationConfiguration by reading the DOM and instantiating * ApplicationConnections accordingly. Call {@link #startNextApplication()} * to actually start the applications. - * + * * @param widgetset * the widgetset that is running the apps */ @@ -171,7 +177,7 @@ public class ApplicationConfiguration { * once to start the first application; after that, each application should * call this once it has started. This ensures that the applications are * started synchronously, which is neccessary to avoid session-id problems. - * + * * @return true if an unstarted application was found */ public static boolean startNextApplication() { diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java index 02c05358e2..4e9e488bb4 100755 --- a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java @@ -46,16 +46,16 @@ import com.vaadin.terminal.gwt.server.AbstractCommunicationManager; * This is the client side communication "engine", managing client-server * communication with its server side counterpart * {@link AbstractCommunicationManager}. - * + * * Client-side widgets receive updates from the corresponding server-side * components as calls to * {@link Paintable#updateFromUIDL(UIDL, ApplicationConnection)} (not to be * confused with the server side interface {@link com.vaadin.terminal.Paintable} * ). Any client-side changes (typically resulting from user actions) are sent * back to the server as variable changes (see {@link #updateVariable()}). - * + * * TODO document better - * + * * Entry point classes (widgetsets) define onModuleLoad(). */ public class ApplicationConnection { @@ -217,7 +217,7 @@ public class ApplicationConnection { *
  • vaadin.postRequestHooks is a map of functions which gets * called after each XHR made by vaadin application. Note, that it is * attaching js functions responsibility to create the variable like this: - * + * *
          * if(!vaadin.postRequestHooks) {vaadin.postRequestHooks = new Object();}
          * postRequestHooks.myHook = function(appId) {
    @@ -228,7 +228,7 @@ public class ApplicationConnection {
          * 
    First parameter passed to these functions is the identifier * of Vaadin application that made the request. * - * + * * TODO make this multi-app aware */ private native void initializeClientHooks() @@ -259,7 +259,7 @@ public class ApplicationConnection { /** * Runs possibly registered client side post request hooks. This is expected * to be run after each uidl request made by Vaadin application. - * + * * @param appId */ private static native void runPostRequestHooks(String appId) @@ -282,7 +282,7 @@ public class ApplicationConnection { /** * Checks if client side is in debug mode. Practically this is invoked by * adding ?debug parameter to URI. - * + * * @return true if client side is currently been debugged */ public native static boolean isDebugMode() @@ -303,6 +303,12 @@ public class ApplicationConnection { return re.test(uri); }-*/; + /** + * Gets the application base URI. Using this other than as the download + * action URI can cause problems in Portlet 2.0 deployments. + * + * @return application base URI + */ public String getAppUri() { return configuration.getApplicationUri(); }; @@ -464,7 +470,7 @@ public class ApplicationConnection { /** * Shows the communication error notification. The 'details' only go to the * console for now. - * + * * @param details * Optional details for debugging. */ @@ -546,7 +552,7 @@ public class ApplicationConnection { /** * This method is called after applying uidl change set to application. - * + * * It will clean current and queued variable change sets. And send next * change set if it exists. */ @@ -566,7 +572,7 @@ public class ApplicationConnection { /** * Cleans given queue of variable changes of such changes that came from * components that do not exist anymore. - * + * * @param variableBurst */ private void cleanVariableBurst(ArrayList variableBurst) { @@ -923,7 +929,7 @@ public class ApplicationConnection { /** * Returns Paintable element by its id - * + * * @param id * Paintable ID */ @@ -957,12 +963,12 @@ public class ApplicationConnection { /** * This method sends currently queued variable changes to server. It is * called when immediate variable update must happen. - * + * * To ensure correct order for variable changes (due servers multithreading * or network), we always wait for active request to be handler before * sending a new one. If there is an active request, we will put varible * "burst" to queue that will be purged after current request is handled. - * + * */ @SuppressWarnings("unchecked") public void sendPendingVariableChanges() { @@ -984,11 +990,11 @@ public class ApplicationConnection { /** * Build the variable burst and send it to server. - * + * * When sync is forced, we also force sending of all pending variable-bursts * at the same time. This is ok as we can assume that DOM will never be * updated after this. - * + * * @param pendingVariables * Vector of variable changes to send * @param forceSync @@ -1143,9 +1149,9 @@ public class ApplicationConnection { /** * Update generic component features. - * + * *

    Selecting correct implementation

    - * + * *

    * The implementation of a component depends on many properties, including * styles, component features, etc. Sometimes the user changes those @@ -1153,21 +1159,21 @@ public class ApplicationConnection { * the beginning of your updateFromUIDL -method automatically replaces your * component with more appropriate if the requested implementation changes. *

    - * + * *

    Caption, icon, error messages and description

    - * + * *

    * Component can delegate management of caption, icon, error messages and * description to parent layout. This is optional an should be decided by * component author *

    - * + * *

    Component visibility and disabling

    - * + * * This method will manage component visibility automatically and if * component is an instanceof FocusWidget, also handle component disabling * when needed. - * + * * @param component * Widget to be updated, expected to implement an instance of * Paintable @@ -1176,7 +1182,7 @@ public class ApplicationConnection { * @param manageCaption * True if you want to delegate caption, icon, description and * error message management to parent. - * + * * @return Returns true iff no further painting is needed by caller */ public boolean updateComponent(Widget component, UIDL uidl, @@ -1386,7 +1392,7 @@ public class ApplicationConnection { /** * Traverses recursively child widgets until ContainerResizedListener child * widget is found. They will delegate it further if needed. - * + * * @param container */ private boolean runningLayout = false; @@ -1449,7 +1455,7 @@ public class ApplicationConnection { /** * Converts relative sizes into pixel sizes. - * + * * @param child * @return true if the child has a relative size */ @@ -1596,7 +1602,7 @@ public class ApplicationConnection { /** * Converts relative sizes into pixel sizes. - * + * * @param child * @return true if the child has a relative size */ @@ -1613,12 +1619,12 @@ public class ApplicationConnection { /** * Get either existing or new Paintable for given UIDL. - * + * * If corresponding Paintable has been previously painted, return it. * Otherwise create and register a new Paintable from UIDL. Caller must * update the returned Paintable from UIDL after it has been connected to * parent. - * + * * @param uidl * UIDL to create Paintable from. * @return Either existing or new Paintable corresponding to UIDL. @@ -1638,7 +1644,7 @@ public class ApplicationConnection { /** * Returns a Paintable element by its root element - * + * * @param element * Root element of the paintable */ @@ -1652,7 +1658,7 @@ public class ApplicationConnection { /** * Singleton method to get instance of app's context menu. - * + * * @return VContextMenu object */ public VContextMenu getContextMenu() { @@ -1668,7 +1674,7 @@ public class ApplicationConnection { * Translates custom protocols in UIDL URI's to be recognizable by browser. * All uri's from UIDL should be routed via this method before giving them * to browser due URI's in UIDL may contain custom protocols like theme://. - * + * * @param uidlUri * Vaadin URI from uidl * @return translated URI ready for browser @@ -1696,7 +1702,7 @@ public class ApplicationConnection { /** * Listens for Notification hide event, and redirects. Used for system * messages, such as session expired. - * + * */ private class NotificationRedirect implements VNotification.EventListener { String url; @@ -1716,9 +1722,9 @@ public class ApplicationConnection { /** * Data showed in tooltips are stored centrilized as it may be needed in * varios place: caption, layouts, and in owner components themselves. - * + * * Updating TooltipInfo is done in updateComponent method. - * + * */ public TooltipInfo getTooltipTitleInfo(Paintable titleOwner, Object key) { if (null == titleOwner) { @@ -1738,9 +1744,9 @@ public class ApplicationConnection { * Component may want to delegate Tooltip handling to client. Layouts add * Tooltip (description, errors) to caption, but some components may want * them to appear one other elements too. - * + * * Events wanted by this handler are same as in Tooltip.TOOLTIP_EVENTS - * + * * @param event * @param owner */ @@ -1753,9 +1759,9 @@ public class ApplicationConnection { * Component may want to delegate Tooltip handling to client. Layouts add * Tooltip (description, errors) to caption, but some components may want * them to appear one other elements too. - * + * * Events wanted by this handler are same as in Tooltip.TOOLTIP_EVENTS - * + * * @param event * @param owner * @param key @@ -1769,7 +1775,7 @@ public class ApplicationConnection { /** * Adds PNG-fix conditionally (only for IE6) to the specified IMG -element. - * + * * @param el * the IMG element to fix */ @@ -1819,7 +1825,7 @@ public class ApplicationConnection { * Reset the name of the current browser-window. This should reflect the * window-name used in the server, but might be different from the * window-object target-name on client. - * + * * @param stringAttribute * New name for the window. */ @@ -1849,14 +1855,14 @@ public class ApplicationConnection { *

    * This method can also be used to deregister tooltips by using null as * tooltip - * + * * @param paintable * Paintable "owning" this tooltip * @param key * key assosiated with given tooltip. Can be any object. For * example a related dom element. Same key must be given for * {@link #handleTooltipEvent(Event, Paintable, Object)} method. - * + * * @param tooltip * the TooltipInfo object containing details shown in tooltip, * null if deregistering tooltip diff --git a/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java b/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java index de25fcece3..b3035a7cc4 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java @@ -260,6 +260,33 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet return defaultValue; } + /** + * Return the URL from where static files, e.g. the widgetset and the theme, + * are served. In a standard configuration the VAADIN folder inside the + * returned folder is what is used for widgetsets and themes. + * + * @param request + * @return The location of static resources (inside which there should be a + * VAADIN directory). Does not end with a slash (/). + */ + private String getStaticFilesLocation(PortletRequest request) { + // TODO allow overriding on portlet level? + String staticFileLocation = getPortalProperty( + Constants.PORTAL_PARAMETER_VAADIN_RESOURCE_PATH, request + .getPortalContext()); + if (staticFileLocation != null) { + // remove trailing slash if any + while (staticFileLocation.endsWith(".")) { + staticFileLocation = staticFileLocation.substring(0, + staticFileLocation.length() - 1); + } + return staticFileLocation; + } else { + // default for Liferay + return "/html"; + } + } + enum RequestType { FILE_UPLOAD, UIDL, RENDER, STATIC_FILE, APPLICATION_RESOURCE, DUMMY, EVENT, UNKNOWN; } @@ -318,18 +345,11 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet protected void handleRequest(PortletRequest request, PortletResponse response) throws PortletException, IOException { - // System.out.println("AbstractApplicationPortlet.handleRequest() " - // + System.currentTimeMillis()); - RequestType requestType = getRequestType(request); - // System.out.println(" RequestType: " + requestType); - // System.out.println(" WindowID: " + request.getWindowID()); - if (requestType == RequestType.UNKNOWN) { System.err.println("Unknown request type"); } else if (requestType == RequestType.DUMMY) { - // System.out.println("Printing Dummy page"); /* * This dummy page is used by action responses to redirect to, in * order to prevent the boot strap code from being rendered into @@ -555,9 +575,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet / 1000); response.setProperty("Expires", "" + System.currentTimeMillis() + cacheTime); - response.setProperty("Pragma", "cache"); // Required to apply - // caching in some - // Tomcats + // Required to apply caching in some Tomcats + response.setProperty("Pragma", "cache"); } // Copy download stream parameters directly @@ -603,8 +622,6 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet final String resourceID = request.getResourceID(); final PortletContext pc = getPortletContext(); - // System.out.println("Trying to load resource [" + resourceID + "]"); - InputStream is = pc.getResourceAsStream(resourceID); if (is != null) { final String mimetype = pc.getMimeType(resourceID); @@ -762,9 +779,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet .getApplicationContext(session); final Collection applications = context.getApplications(); - for (final Iterator i = applications.iterator(); i - .hasNext();) { - final Application sessionApplication = i.next(); + for (Application sessionApplication : applications) { if (request.getWindowID().equals( sessionApplication.getPortletWindowId())) { if (sessionApplication.isRunning()) { @@ -780,30 +795,30 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet } protected String getWidgetsetURL(String widgetset, PortletRequest request) { - // TODO The widgetset URL is currently hard-corded for LifeRay - return "/html/" + WIDGETSET_DIRECTORY_PATH + widgetset + "/" + return getStaticFilesLocation(request) + "/" + WIDGETSET_DIRECTORY_PATH + + widgetset + "/" + widgetset + ".nocache.js?" + new Date().getTime(); } protected String getThemeURI(String themeName, PortletRequest request) { - // TODO The theme URI is currently hard-corded for LifeRay - return "/html/" + THEME_DIRECTORY_PATH + themeName; + return getStaticFilesLocation(request) + "/" + THEME_DIRECTORY_PATH + + themeName; } protected void writeAjaxPage(RenderRequest request, RenderResponse response, Window window, Application application) throws IOException, MalformedURLException, PortletException { - System.out.println("writeAjaxPage"); - response.setContentType("text/html"); final BufferedWriter page = new BufferedWriter(new OutputStreamWriter( response.getPortletOutputStream(), "UTF-8")); + // TODO this is broken, not set as no servlet String requestWidgetset = (String) request .getAttribute(AbstractApplicationServlet.REQUEST_WIDGETSET); - String sharedWidgetset = (String) request - .getAttribute(AbstractApplicationServlet.REQUEST_SHARED_WIDGETSET); + + String sharedWidgetset = getPortalProperty( + PORTAL_PARAMETER_VAADIN_WIDGETSET, request.getPortalContext()); if (requestWidgetset == null && sharedWidgetset == null) { requestWidgetset = getApplicationOrSystemProperty( PARAMETER_WIDGETSET, DEFAULT_WIDGETSET); @@ -823,10 +838,6 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet String widgetsetURL = getWidgetsetURL(widgetset, request); String themeURI = getThemeURI(themeName, request); - //System.out.println("themeName : " + themeName); - //System.out.println("widgetsetURL : " + widgetsetURL); - //System.out.println("themeURI : " + themeURI); - // fixed base theme to use - all portal pages with Vaadin // applications will load this exactly once String portalTheme = getPortalProperty(PORTAL_PARAMETER_VAADIN_THEME, @@ -861,9 +872,6 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet /* * We need this in order to get uploads to work. - * - * TODO This may cause weird side effects on other places where appUri - * is used! */ PortletURL appUri = response.createActionURL(); @@ -908,8 +916,6 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet portalTheme = DEFAULT_THEME_NAME; } -// System.out.println("Printing default portal theme " + portalTheme); - page.write("if(!vaadin.themesLoaded['" + portalTheme + "']) {\n"); page.write("var defaultStylesheet = document.createElement('link');\n"); page.write("defaultStylesheet.setAttribute('rel', 'stylesheet');\n"); @@ -973,24 +979,21 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet // Finds theme name String themeName; - if (request.getParameter(URL_PARAMETER_THEME) != null) { - themeName = request.getParameter(URL_PARAMETER_THEME); - } else { - themeName = window.getTheme(); + // theme defined for the window? + themeName = window.getTheme(); + + if (themeName == null) { + // no, is the default theme defined by the portal? + themeName = getPortalProperty( + Constants.PORTAL_PARAMETER_VAADIN_THEME, request + .getPortalContext()); } if (themeName == null) { - // no explicit theme for window defined - if (request - .getAttribute(AbstractApplicationServlet.REQUEST_DEFAULT_THEME) != null) { - // the default theme is defined in request (by portal) - themeName = (String) request - .getAttribute(AbstractApplicationServlet.REQUEST_DEFAULT_THEME); - } else { - // using the default theme defined by Vaadin - themeName = DEFAULT_THEME_NAME; - } + // no, using the default theme defined by Vaadin + themeName = DEFAULT_THEME_NAME; } + return themeName; } @@ -1149,7 +1152,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet out.flush(); } - private String getPortalProperty(String name, PortalContext context) { + private static String getPortalProperty(String name, PortalContext context) { boolean isLifeRay = context.getPortalInfo().toLowerCase().contains( "liferay"); @@ -1165,7 +1168,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet return value; } - private String getLifeRayPortalProperty(String name) { + private static String getLifeRayPortalProperty(String name) { String value; try { value = PropsUtil.get(name); diff --git a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java index e5ef272af9..9fdaac1bff 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java @@ -1175,15 +1175,15 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements * @param request * @return The location of static resources (should contain the VAADIN * directory). Never ends with a slash (/). - * @throws MalformedURLException */ - String getStaticFilesLocation(HttpServletRequest request) { + private String getStaticFilesLocation(HttpServletRequest request) { // request may have an attribute explicitly telling location (portal // case) String staticFileLocation = (String) request .getAttribute(REQUEST_VAADIN_STATIC_FILE_PATH); if (staticFileLocation != null) { + // TODO remove trailing slash if any? return staticFileLocation; } diff --git a/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java b/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java index e7e2bb3cfb..e53d5d8d25 100644 --- a/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java +++ b/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java @@ -30,11 +30,6 @@ public class ApplicationPortlet implements Portlet, Serializable { private static final String PORTLET_PARAMETER_STYLE = "style"; private static final String PORTLET_PARAMETER_WIDGETSET = "widgetset"; - // portal configuration parameters - private static final String PORTAL_PARAMETER_VAADIN_WIDGETSET = "vaadin.widgetset"; - private static final String PORTAL_PARAMETER_VAADIN_RESOURCE_PATH = "vaadin.resources.path"; - private static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme"; - // The application to show protected String app = null; // some applications might require forced height (and, more seldom, width) @@ -97,16 +92,17 @@ public class ApplicationPortlet implements Portlet, Serializable { // fixed base theme to use - all portal pages with Vaadin // applications will load this exactly once String portalTheme = getPortalProperty( - PORTAL_PARAMETER_VAADIN_THEME, portalCtx); + Constants.PORTAL_PARAMETER_VAADIN_THEME, portalCtx); String portalWidgetset = getPortalProperty( - PORTAL_PARAMETER_VAADIN_WIDGETSET, portalCtx); + Constants.PORTAL_PARAMETER_VAADIN_WIDGETSET, portalCtx); // location of the widgetset(s) and default theme (to which // /VAADIN/widgetsets/... // is appended) String portalResourcePath = getPortalProperty( - PORTAL_PARAMETER_VAADIN_RESOURCE_PATH, portalCtx); + Constants.PORTAL_PARAMETER_VAADIN_RESOURCE_PATH, + portalCtx); if (portalResourcePath != null) { // if portalResourcePath is defined, set it as a request diff --git a/src/com/vaadin/terminal/gwt/server/Constants.java b/src/com/vaadin/terminal/gwt/server/Constants.java index 1a91cc885c..a2825de87a 100644 --- a/src/com/vaadin/terminal/gwt/server/Constants.java +++ b/src/com/vaadin/terminal/gwt/server/Constants.java @@ -2,9 +2,9 @@ package com.vaadin.terminal.gwt.server; /** * TODO Document me! - * + * * @author peholmst - * + * */ public interface Constants { @@ -19,7 +19,7 @@ public interface Constants { + "===========================================================\n" + "WARNING: Cross-site request forgery protection is disabled!\n" + "==========================================================="; - + static final String URL_PARAMETER_RESTART_APPLICATION = "restartApplication"; static final String URL_PARAMETER_CLOSE_APPLICATION = "closeApplication"; static final String URL_PARAMETER_REPAINT_ALL = "repaintAll"; @@ -56,4 +56,9 @@ public interface Constants { static final String INVALID_SECURITY_KEY_MSG = "Invalid security key."; + // portal configuration parameters + static final String PORTAL_PARAMETER_VAADIN_WIDGETSET = "vaadin.widgetset"; + static final String PORTAL_PARAMETER_VAADIN_RESOURCE_PATH = "vaadin.resources.path"; + static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme"; + } diff --git a/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java b/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java index a8814b4c88..c3c319724d 100644 --- a/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java +++ b/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java @@ -27,13 +27,14 @@ import javax.portlet.ResourceResponse; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; - import com.vaadin.Application; import com.vaadin.service.ApplicationContext; /** * TODO Write documentation, fix JavaDoc tags. - * + * + * TODO is implementing HttpSessionBindingListener correct/useful? + * * @author peholmst */ @SuppressWarnings("serial") @@ -52,13 +53,6 @@ public class PortletApplicationContext2 implements ApplicationContext, protected HashMap applicationToAjaxAppMgrMap = new HashMap(); - public void addTransactionListener(TransactionListener listener) { - if (listeners == null) { - listeners = new LinkedList(); - } - listeners.add(listener); - } - public Collection getApplications() { return Collections.unmodifiableCollection(applications); } @@ -79,6 +73,13 @@ public class PortletApplicationContext2 implements ApplicationContext, return null; } + public void addTransactionListener(TransactionListener listener) { + if (listeners == null) { + listeners = new LinkedList(); + } + listeners.add(listener); + } + public void removeTransactionListener(TransactionListener listener) { if (listeners != null) { listeners.remove(listener); -- 2.39.5