[seam-commits] Seam SVN: r10309 - in trunk/examples/wiki: src/etc/WEB-INF and 5 other directories.

seam-commits at lists.jboss.org seam-commits at lists.jboss.org
Mon Apr 6 03:20:49 EDT 2009


Author: christian.bauer at jboss.com
Date: 2009-04-06 03:20:49 -0400 (Mon, 06 Apr 2009)
New Revision: 10309

Added:
   trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetFlushEventListener.java
   trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetLockTimeoutException.java
Removed:
   trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetMonitor.java
Modified:
   trunk/examples/wiki/src/etc/META-INF/persistence-dev-war.xml
   trunk/examples/wiki/src/etc/META-INF/persistence-prod-war.xml
   trunk/examples/wiki/src/etc/META-INF/persistence-test-war.xml
   trunk/examples/wiki/src/etc/WEB-INF/pages.xml
   trunk/examples/wiki/src/etc/i18n/messages_en.properties
   trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/action/Pager.java
   trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetOperation.java
   trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostDeleteEventListener.java
   trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostInsertEventListener.java
   trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/package.html
   trunk/examples/wiki/view/includes/pager.xhtml
Log:
JBSEAM-4069, new approach to nested set locking

Modified: trunk/examples/wiki/src/etc/META-INF/persistence-dev-war.xml
===================================================================
--- trunk/examples/wiki/src/etc/META-INF/persistence-dev-war.xml	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/etc/META-INF/persistence-dev-war.xml	2009-04-06 07:20:49 UTC (rev 10309)
@@ -76,6 +76,8 @@
                       value="read-write, WikiFeedEntryCollection"/>
 
             <!-- Nested Set handling through special Hibernate event listeners -->
+            <property name="hibernate.ejb.event.flush"
+                      value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetFlushEventListener"/>
             <property name="hibernate.ejb.event.post-insert"
                       value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetPostInsertEventListener"/>
             <property name="hibernate.ejb.event.post-delete"

Modified: trunk/examples/wiki/src/etc/META-INF/persistence-prod-war.xml
===================================================================
--- trunk/examples/wiki/src/etc/META-INF/persistence-prod-war.xml	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/etc/META-INF/persistence-prod-war.xml	2009-04-06 07:20:49 UTC (rev 10309)
@@ -73,6 +73,8 @@
                       value="read-write, WikiFeedEntryCollection"/>
 
             <!-- Nested Set handling through special Hibernate event listeners -->
+            <property name="hibernate.ejb.event.flush"
+                      value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetFlushEventListener"/>
             <property name="hibernate.ejb.event.post-insert"
                       value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetPostInsertEventListener"/>
             <property name="hibernate.ejb.event.post-delete"

Modified: trunk/examples/wiki/src/etc/META-INF/persistence-test-war.xml
===================================================================
--- trunk/examples/wiki/src/etc/META-INF/persistence-test-war.xml	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/etc/META-INF/persistence-test-war.xml	2009-04-06 07:20:49 UTC (rev 10309)
@@ -48,6 +48,8 @@
             <property name="hibernate.session_factory_name" value="SessionFactories/lacewikiSF"/>
 
             <!-- Nested Set handling through special Hibernate event listeners -->
+            <property name="hibernate.ejb.event.flush"
+                      value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetFlushEventListener"/>
             <property name="hibernate.ejb.event.post-insert"
                       value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetPostInsertEventListener"/>
             <property name="hibernate.ejb.event.post-delete"

Modified: trunk/examples/wiki/src/etc/WEB-INF/pages.xml
===================================================================
--- trunk/examples/wiki/src/etc/WEB-INF/pages.xml	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/etc/WEB-INF/pages.xml	2009-04-06 07:20:49 UTC (rev 10309)
@@ -335,6 +335,21 @@
         </redirect>
     </exception>
 
+    <exception class="org.jboss.seam.wiki.core.nestedset.listener.NestedSetLockTimeoutException">
+        <end-conversation/>
+        <redirect view-id="/wiki.xhtml">
+            <message severity="WARN">#{messages['lacewiki.msg.LockTimeoutError']}</message>
+        </redirect>
+    </exception>
+
+    <!-- This occurs on concurrent delete of comments -->
+    <exception class="javax.persistence.EntityNotFoundException">
+        <end-conversation/>
+        <redirect view-id="/wiki.xhtml">
+            <message severity="WARN">#{messages['lacewiki.msg.EntityNotFound']}</message>
+        </redirect>
+    </exception>
+
     <exception class="org.jboss.seam.security.AuthorizationException">
         <end-conversation/>
         <redirect view-id="/message.xhtml">

Modified: trunk/examples/wiki/src/etc/i18n/messages_en.properties
===================================================================
--- trunk/examples/wiki/src/etc/i18n/messages_en.properties	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/etc/i18n/messages_en.properties	2009-04-06 07:20:49 UTC (rev 10309)
@@ -267,10 +267,6 @@
 lacewiki.label.dirDisplay.ReadAccess=Read Access
 lacewiki.label.dirDisplay.WriteAccess=Write Access
 lacewiki.label.dirDisplay.DirectoryIsEmpty=This directory is empty.
-lacewiki.label.dirDisplay.PagerShowing=
-lacewiki.label.dirDisplay.PagerTo=to
-lacewiki.label.dirDisplay.PagerOf=of
-lacewiki.label.dirDisplay.PagerElements=
 lacewiki.label.dirDisplay.ShowItems=Show items
 lacewiki.label.dirDisplay.All=All
 lacewiki.button.dirDisplay.Refresh=Re<u>f</u>resh
@@ -652,6 +648,9 @@
 lacewiki.msg.Clipboard.DuplicatePasteName=The name '{0}' was already in use in this area, renamed item to '{1}'.
 lacewiki.msg.Clipboard.DuplicatePasteNameFailure=The name '{0}' was already in use in this area and is too long to be renamed, skipping paste.
 
+# Pager
+lacewiki.label.pagerTo=to
+lacewiki.label.pagerOf=of
 
 # Entity update/delete/persist
 
@@ -717,6 +716,8 @@
 lacewiki.msg.TrashAreaNotFound=Could not find trash area with name {0}  - your configuration is broken, please change it.
 lacewiki.msg.HelpAreaNotFound=Could not find help area with name {0}  - your configuration is broken, please change it.
 lacewiki.msg.OptimisticLockError=Someone modified the same record while you were editing it. Your workspace has been closed.
+lacewiki.msg.LockTimeoutError=Your action conflicted with the action of another user, please try again in a few seconds.
+lacewiki.msg.EntityNotFound=The requested entity was not found.
 lacewiki.msg.AccessDenied=Access Denied
 lacewiki.msg.FatalError=Request failed, please check the application log or contact the administrator
 lacewiki.msg.RequestError=Request failed, most likely because a request parameter was missing

Modified: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/action/Pager.java
===================================================================
--- trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/action/Pager.java	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/action/Pager.java	2009-04-06 07:20:49 UTC (rev 10309)
@@ -14,6 +14,7 @@
 @AutoCreate
 public class Pager implements Serializable {
 
+    private String pagerEventName = "Pager";
     private Long numOfRecords = 0l;
     private Integer page = 0;
     private Long pageSize = 15l;
@@ -24,6 +25,15 @@
         this.pageSize = pageSize;
     }
 
+    public Pager(String pagerEventName) {
+        this.pagerEventName = pagerEventName;
+    }
+
+    public Pager(String pagerEventName, Long pageSize) {
+        this.pagerEventName = pagerEventName;
+        this.pageSize = pageSize;
+    }
+
     public Long getNumOfRecords() {
         return numOfRecords;
     }
@@ -103,22 +113,22 @@
 
     public void setFirstPage() {
         setPage(getFirstPage());
-        Events.instance().raiseEvent("Pager.pageChanged");
+        Events.instance().raiseEvent(pagerEventName + "pageChanged");
     }
 
     public void setPreviousPage() {
         setPage(getPreviousPage());
-        Events.instance().raiseEvent("Pager.pageChanged");
+        Events.instance().raiseEvent(pagerEventName + ".pageChanged");
     }
 
     public void setNextPage() {
         setPage(getNextPage());
-        Events.instance().raiseEvent("Pager.pageChanged");
+        Events.instance().raiseEvent(pagerEventName + ".pageChanged");
     }
 
     public void setLastPage() {
         setPage(new Long(getLastPage()).intValue());
-        Events.instance().raiseEvent("Pager.pageChanged");
+        Events.instance().raiseEvent(pagerEventName + ".pageChanged");
     }
 
     public String toString() {

Added: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetFlushEventListener.java
===================================================================
--- trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetFlushEventListener.java	                        (rev 0)
+++ trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetFlushEventListener.java	2009-04-06 07:20:49 UTC (rev 10309)
@@ -0,0 +1,105 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package org.jboss.seam.wiki.core.nestedset.listener;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.hibernate.HibernateException;
+import org.hibernate.ejb.event.EJB3FlushEventListener;
+import org.hibernate.event.EventSource;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * <p>
+ * TODO: This is really the issue why a nice nested set implementation with Hibernate and MySQL is impossible!
+ * </p>
+ * <p>
+ * Any nested set tree modification potentially updates all rows in a database table. This
+ * requires several <tt>UPDATE</tt> statements, and also <tt>INSERT</tt> and <tt>DELETE</tt>.
+ * Any concurrent insertion or deletion to the rows betwen <tt>UPDATE</tt> statements would be fatal and
+ * corrupt the tree information. Transactions that modify nested set data should be serialized.
+ * </p>
+ * <p>
+ * For example, without any additional locking we'd run into the following deadlock situation with MySQL. Consider
+ * the following threads and the required modifications when two nested set node are deleted concurrently:
+ * </p>
+ *
+ * <pre>
+ *    Thread I         Nested Set Nodes       Thread II
+ * --- 1. DELETE --->         A
+ *
+ *                            B           <--- 2. DELETE ---
+ *
+ * --- 2. UPDATE --->         B
+ *  (Waits for lock)
+ *                            A           <--- 3. UPDATE ---
+ *                                          (Waits for lock)
+ *
+ * </pre>
+ *
+ * <p>
+ * This results in a MySQL deadlock detection exception and a rollback of the transaction in Thread II.
+ * The usual solution is to lock the whole table(s) to force serialized execution of threads that modify
+ * nested set tree state.
+ * </p>
+ * <p>
+ * However, because MySQL has an unusable locking system (locking a table commits the current transaction, you
+ * need to lock all tables you are going to use from that point on, etc.), and because portability is
+ * a concern of this nested set implementation, we work around the problem with an in-memory exclusive lock.
+ * </p>
+ * <p>
+ * The situation is further complicated by Hibernate's flushing/eventing behavior. There is no way how we can
+ * only lock for nested set updates, we need to lock on every execution of a flush. WARNING: This severely
+ * degrades performance of your application, as any automatic or manual flush of the Hibernate persistence context
+ * will be serialized application-wide! Luckily, we can only lock when deletions or insertions are queued so
+ * flushing with no modifications in the persistence context (e.g. before a query) is not affected.
+ * </p>
+ * <p>
+ * <b>NOTE:</b> This does NOT work if several applications modify the nested set tree on the same database tables!
+ * </p>
+ *
+ * @author Christian Bauer
+ */
+public class NestedSetFlushEventListener extends EJB3FlushEventListener {
+
+    private static final Log log = LogFactory.getLog(NestedSetFlushEventListener.class);
+
+    private static final int LOCK_TIMEOUT_SECONDS = 15;
+
+    private static final Lock lock = new ReentrantLock(true);
+
+    @Override
+    protected void performExecutions(EventSource eventSource) throws HibernateException {
+
+        if (eventSource.getActionQueue().areInsertionsOrDeletionsQueued()) {
+            try {
+                log.debug("######################### trying to obtain exclusive lock for " + LOCK_TIMEOUT_SECONDS +
+                          " seconds before performing database modifications during flush");
+                if (lock.tryLock(LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+                    log.debug("successfully obtained lock, executing flush");
+                    try {
+                        super.performExecutions(eventSource);
+                    } finally {
+                        log.debug("releasing exclusive lock after flush execution");
+                        lock.unlock();
+                    }
+                } else {
+                    throw new NestedSetLockTimeoutException("Could not aquire exclusive lock during database flush");
+                }
+            } catch (InterruptedException ex) {
+                throw new NestedSetLockTimeoutException("Current thread could not aquire lock, has been interrupted");
+            }
+        } else {
+            super.performExecutions(eventSource);
+        }
+
+    }
+
+}
\ No newline at end of file

Added: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetLockTimeoutException.java
===================================================================
--- trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetLockTimeoutException.java	                        (rev 0)
+++ trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetLockTimeoutException.java	2009-04-06 07:20:49 UTC (rev 10309)
@@ -0,0 +1,23 @@
+package org.jboss.seam.wiki.core.nestedset.listener;
+
+/**
+ * @author Christian Bauer
+ */
+public class NestedSetLockTimeoutException extends RuntimeException {
+
+    public NestedSetLockTimeoutException() {
+        super();
+    }
+
+    public NestedSetLockTimeoutException(String s) {
+        super(s);
+    }
+
+    public NestedSetLockTimeoutException(String s, Throwable throwable) {
+        super(s, throwable);
+    }
+
+    public NestedSetLockTimeoutException(Throwable throwable) {
+        super(throwable);
+    }
+}

Deleted: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetMonitor.java
===================================================================
--- trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetMonitor.java	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetMonitor.java	2009-04-06 07:20:49 UTC (rev 10309)
@@ -1,49 +0,0 @@
-package org.jboss.seam.wiki.core.nestedset.listener;
-
-import org.hibernate.event.EventSource;
-
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.TimeUnit;
-
-/**
- * An alternative to table locking, we serialize nested set insert/updates in memory.
- *
- * <p>
- * Any nested set tree modification potentially updates all rows in a database table. This
- * requires several <tt>UPDATE</tt> statements, and also <tt>INSERT</tt> and <tt>DELETE</tt>.
- * Any concurrent commit to the rows betwen <tt>UPDATE</tt> statements would be fatal and
- * corrupt the tree information. The usual solution is to lock the whole table(s). Because
- * MySQL has a compleltey unusable locking system (locking a table commits the current transaction, you
- * need to lock all tables you are going to use from that point on, etc.), and because portability is
- * a concern of this Nested Set implementation, we work around the problem with an in-memory exclusive lock.
- * </p>
- * <p>
- * <b>NOTE:</b> This does NOT work if several applications modify the nested set
- * tree in the same tables!
- * </p>
- *
- * @author Christian Bauer
- */
-public class NestedSetMonitor {
-
-    private static final int LOCK_TIMEOUT_SECONDS = 10;
-
-    private static final Lock lock = new ReentrantLock(true);
-
-    public static void executeOperation(NestedSetOperation operation, EventSource session) {
-        try {
-            if (lock.tryLock(LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
-                try {
-                    operation.execute(session);
-                } finally {
-                    lock.unlock();
-                }
-            } else {
-                throw new RuntimeException("Could not aquire lock to update nested set tree");
-            }
-        } catch (InterruptedException ex) {
-            throw new RuntimeException("Current thread could not aquire lock, has been interrupted");
-        }
-    }
-}

Modified: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetOperation.java
===================================================================
--- trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetOperation.java	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetOperation.java	2009-04-06 07:20:49 UTC (rev 10309)
@@ -43,8 +43,6 @@
  * for the deprecated <tt>Session#connection()</tt> method.
  * </p>
  *
- * TODO: We should lock the tables! Instead we are using the NestedSetMonitor as a workaround...
- *
  * @author Christian Bauer
  */
 public class NestedSetOperation {

Modified: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostDeleteEventListener.java
===================================================================
--- trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostDeleteEventListener.java	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostDeleteEventListener.java	2009-04-06 07:20:49 UTC (rev 10309)
@@ -26,10 +26,7 @@
 
         if ( NestedSetNode.class.isAssignableFrom(event.getEntity().getClass())) {
             log.debug("executing nested set delete operation, recalculating the tree");
-            NestedSetMonitor.executeOperation(
-                new DeleteNestedSetOperation( (NestedSetNode)event.getEntity() ),
-                event.getSession()
-            );
+            new DeleteNestedSetOperation( (NestedSetNode)event.getEntity() ).execute(event.getSession());
         }
     }
 

Modified: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostInsertEventListener.java
===================================================================
--- trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostInsertEventListener.java	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostInsertEventListener.java	2009-04-06 07:20:49 UTC (rev 10309)
@@ -26,10 +26,7 @@
 
         if ( NestedSetNode.class.isAssignableFrom(event.getEntity().getClass())) {
             log.debug("executing nested set insert operation, recalculating the tree");
-            NestedSetMonitor.executeOperation(
-                new InsertNestedSetOperation((NestedSetNode)event.getEntity()),
-                event.getSession()
-            );
+            new InsertNestedSetOperation((NestedSetNode)event.getEntity()).execute(event.getSession());
         }
     }
 

Modified: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/package.html
===================================================================
--- trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/package.html	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/package.html	2009-04-06 07:20:49 UTC (rev 10309)
@@ -59,7 +59,7 @@
         <p>
         <b>Manual Recursion</b>: Select all the children of node <tt>C</tt>, and if these nodes have children, recursively
         query until the whole subtree is loaded. This can be implemented with a stored procedure in SQL or by
-        recursively exeucting <tt>SELECT</tt> statements in the application language. This strategy does not scale.
+        recursively executing <tt>SELECT</tt> statements in the application language. This strategy does not scale.
         </p>
     </li>
     <li>
@@ -201,7 +201,7 @@
 </pre>
 
 <p>
-    This implementation is based on mix-in and delegates. The <tt>ITEM</tt> table of the <tt>Item</tt> class will
+    This implementation is based on mix-in and delegates. The <tt>ITEM</tt> table of the <tt>Item</tt> class
     does not carry the left, right, and thread values. This job is delegated to an additional
     <tt>ItemNestedSetDelegate</tt> class:
 </p>
@@ -280,11 +280,12 @@
     &lt;persistence-unit ...>
         ...
         &lt;properties>
+            &lt;property name="hibernate.ejb.event.flush"
+                      value="nestedset.NestedSetFlushEventListener"/>
             &lt;property name="hibernate.ejb.event.post-insert"
                       value="nestedset.NestedSetPostInsertEventListener"/>
             &lt;property name="hibernate.ejb.event.post-delete"
                       value="nestedset.NestedSetPostDeleteEventListener"/>
-
         &lt;/properties>
     &lt;/persistence-unit>
 </pre>
@@ -296,6 +297,12 @@
 </p>
 
 <p>
+    Note that concurrent nested set tree modifications need to be serialized. This implementation locks flush events
+    in-memory with a <tt>ReentrantLock</tt> and a timeout of 15 seconds. See <tt>NestedSetFlushEventListener.java</tt>
+    for more information.
+</p>
+
+<p>
     To query for a subtree, use the <tt>NestedSetWrapper</tt> and <tt>NestedSetResultTransformer</tt>
     convenience classes. An example, loading the whole subtree starting at <tt>startNode</tt> (which would
     be an instance of <tt>Item</tt> you have already loaded):

Modified: trunk/examples/wiki/view/includes/pager.xhtml
===================================================================
--- trunk/examples/wiki/view/includes/pager.xhtml	2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/view/includes/pager.xhtml	2009-04-06 07:20:49 UTC (rev 10309)
@@ -48,10 +48,10 @@
         </s:fragment>
 
         <s:fragment>
-            <h:outputText value="#{messages['lacewiki.label.dirDisplay.PagerShowing']}
-                                    #{pager.firstRecord} #{messages['lacewiki.label.dirDisplay.PagerTo']}
-                                    #{pager.lastRecord} #{messages['lacewiki.label.dirDisplay.PagerOf']}
-                                    #{pager.numOfRecords} #{messages['lacewiki.label.dirDisplay.PagerElements']}"/>
+            <h:outputText value="#{pager.firstRecord} #{messages['lacewiki.label.pagerTo']}
+                                 #{pager.lastRecord} #{messages['lacewiki.label.pagerOf']}
+                                 #{pager.numOfRecords}
+                                 #{pager.numOfRecords > 1 ? pagerPluralLabel : pagerSingularLabel}"/>
         </s:fragment>
 
         <s:fragment>




More information about the seam-commits mailing list