001    /*
002    Copyright (c) 1996-2012, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    package org.hd.d.pg2k.test.dev;
031    
032    import java.io.File;
033    import java.util.ArrayList;
034    import java.util.Collection;
035    import java.util.Collections;
036    import java.util.HashSet;
037    import java.util.Iterator;
038    import java.util.List;
039    import java.util.Map;
040    import java.util.concurrent.ConcurrentHashMap;
041    import java.util.concurrent.ConcurrentMap;
042    import java.util.concurrent.atomic.AtomicInteger;
043    
044    import junit.framework.TestCase;
045    
046    import org.hd.d.pg2k.svrCore.AllExhibitProperties;
047    import org.hd.d.pg2k.svrCore.FileTools;
048    import org.hd.d.pg2k.svrCore.GenUtils;
049    import org.hd.d.pg2k.svrCore.Name;
050    import org.hd.d.pg2k.svrCore.Name.ExhibitFull;
051    import org.hd.d.pg2k.webSvr.catalogue.PaginationBeanBase;
052    import org.hd.d.pg2k.webSvr.catalogue.PaginationBeanNumeric;
053    import org.hd.d.pg2k.webSvr.catalogue.PaginationBeanTree;
054    import org.hd.d.pg2k.webSvr.exhibit.TreeFilterBean;
055    import org.hd.d.pg2k.webSvr.util.WebConsts;
056    
057    /**
058     * Created by IntelliJ IDEA.
059     * User: Damon Hart-Davis
060     * Date: 03-Jun-2003
061     * Time: 17:55:04
062     */
063    
064    /**Tests the variants and derivatives of PaginationBeanNumeric.
065     * These beans can be used stand-alone (outside a JSP page) at least in part,
066     * so we can test the core algorithms.
067     */
068    public final class PaginationBeanTest extends TestCase
069        {
070        /**Performs test of the base bean.
071         * This ensures that the basic underlying algorithms are correct.
072         */
073        public void testPaginationBeanBase()
074            {
075            // First just create an instance of the bean.
076            final PaginationBeanNumeric pbn = new PaginationBeanNumeric();
077    
078            // Test initial values.
079            assertTrue("default number of items should be zero", pbn.getNumberOfItems() == 0);
080            assertTrue("default page number should be 1", pbn.getPg() == 1);
081            // Do the generic tests with current setup.
082            testPBContract(pbn);
083    
084            // Check that any attmept to set the page with zero items to paginate is quetly ignored.
085            pbn.setPg(-1);
086            assertTrue("page number should be 1", pbn.getPg() == 1);
087            pbn.setPg(9);
088            assertTrue("page number should be 1", pbn.getPg() == 1);
089            pbn.setPg("rubbish");
090            assertTrue("page number should be 1", pbn.getPg() == 1);
091    
092            // Check number-of-pages algorithm.
093            pbn.setNumberOfItems(0);
094            assertTrue("0 items needs zero pages", pbn.getNumberOfPages() == 0);
095            pbn.setNumberOfItems(1);
096            assertTrue("1 item needs one page", pbn.getNumberOfPages() == 1);
097            pbn.setNumberOfItems(WebConsts.MAX_RESULTS_PER_PAGE - 1);
098            assertTrue("1 item less than the max per page needs one page", pbn.getNumberOfPages() == 1);
099            pbn.setNumberOfItems(WebConsts.MAX_RESULTS_PER_PAGE);
100            assertTrue("the max per page needs one page", pbn.getNumberOfPages() == 1);
101            pbn.setNumberOfItems(WebConsts.MAX_RESULTS_PER_PAGE + 1);
102            assertTrue("1 item more than the max per page needs two pages", pbn.getNumberOfPages() == 2);
103            pbn.setNumberOfItems(99 * WebConsts.MAX_RESULTS_PER_PAGE + 1);
104            assertTrue("wrong result for many items", pbn.getNumberOfPages() == 100);
105            // Do the generic tests with current setup.
106            testPBContract(pbn);
107    
108            // Test the (plain-old-int) page-setting algorithm.
109            pbn.setNumberOfItems(10 * WebConsts.MAX_RESULTS_PER_PAGE);
110            assertTrue("wrong result for ten-pages' worth of items", pbn.getNumberOfPages() == 10);
111            pbn.setPg(3);
112            assertTrue("did not set page number correctly to 3", pbn.getPg() == 3);
113            pbn.setPg(-999);
114            assertTrue("did not force page number correctly to 1 (always safe)", pbn.getPg() == 1);
115            pbn.setPg(7);
116            assertTrue("did not set page number correctly to 7", pbn.getPg() == 7);
117            pbn.setPg(999);
118            assertTrue("did not force page number correctly to 1 (always safe)", pbn.getPg() == 1);
119            pbn.setPg(10);
120            assertTrue("did not set page number correctly to 10 (upper limit)", pbn.getPg() == 10);
121            pbn.setPg(1);
122            assertTrue("did not set page number correctly to 1 (lower limit)", pbn.getPg() == 1);
123            // Test the String argument version.
124            pbn.setPg("4");
125            assertTrue("did not set page number correctly to 4", pbn.getPg() == 4);
126            pbn.setPg("broken");
127            assertTrue("did not force page number correctly to 1 (always safe)", pbn.getPg() == 1);
128            // Do the generic tests with current setup.
129            testPBContract(pbn);
130    
131            // Test item upper/lower bound algorithm
132            pbn.setNumberOfItems(2 + 3 * WebConsts.MAX_RESULTS_PER_PAGE);
133            assertTrue("wrong result for four pages' worth of items", pbn.getNumberOfPages() == 4);
134            pbn.setPg(1);
135            assertTrue("lower bound for first page wrong", pbn.getStartIndex() == 0);
136            assertTrue("upper bound for first page wrong", pbn.getEndIndex() == WebConsts.MAX_RESULTS_PER_PAGE);
137            pbn.setPg(2);
138            assertTrue("lower bound for second page wrong", pbn.getStartIndex() == WebConsts.MAX_RESULTS_PER_PAGE);
139            assertTrue("upper bound for second page wrong", pbn.getEndIndex() == 2 * WebConsts.MAX_RESULTS_PER_PAGE);
140            pbn.setPg(4);
141            assertTrue("lower bound for last (partial) page wrong", pbn.getStartIndex() == 3 * WebConsts.MAX_RESULTS_PER_PAGE);
142            assertTrue("upper bound for last (partial) page wrong", pbn.getEndIndex() == pbn.getNumberOfItems());
143            // Do the generic tests with current setup.
144            testPBContract(pbn);
145    
146            // Test label generation for smallish number of pages
147            // (less than the maximum we can do buttons for in one go).
148            pbn.setNumberOfItems(1 + 2 * WebConsts.MAX_RESULTS_PER_PAGE);
149            assertTrue("Check label generation for page 2", "2".equals(pbn.getPageLabel(2)));
150            assertTrue("Check label generation for page 1", "1".equals(pbn.getPageLabel(1)));
151            assertTrue("Check label generation for page 3", "3".equals(pbn.getPageLabel(3)));
152            final List<String> l1 = pbn.getPageLabels();
153            assertTrue("Check length of label list (3)", l1.size() == 3);
154            assertTrue("Check labels are right for the pages on small list",
155                       "1".equals(l1.get(0)) &&
156                       "2".equals(l1.get(1)) &&
157                       "3".equals(l1.get(2)));
158            // Do the generic tests with current setup.
159            testPBContract(pbn);
160    
161            // Test label generation for large number of items
162            // (too many to generate page buttons for all at once).
163            pbn.setNumberOfItems(1 + 2 * WebConsts.MAX_RESULTS_PAGES * WebConsts.MAX_RESULTS_PER_PAGE);
164            assertTrue("Check label generation for page 2", "2".equals(pbn.getPageLabel(2)));
165            assertTrue("Check label generation for page 1", "1".equals(pbn.getPageLabel(1)));
166            assertTrue("Check label generation for page 3", "3".equals(pbn.getPageLabel(3)));
167            assertTrue("Check label generation for page 17: got "+pbn.getPageLabel(17), "17".equals(pbn.getPageLabel(17)));
168            final List<String> l2 = pbn.getPageLabels();
169            assertTrue("Check length of label list for large set", l2.size() <= WebConsts.MAX_RESULTS_PAGES);
170            // Check first and last pages have labels,
171            // and rest are monotonically increasing.
172            assertTrue("Check first and last labels are right for the pages on large list",
173                       "1".equals(l1.get(0)) &&
174                       String.valueOf(pbn.getNumberOfPages()).equals(l2.get(l2.size() - 1)));
175            int lastP1 = 0;
176            for(final Iterator<String> it = l2.iterator(); it.hasNext(); )
177                {
178                final int n = Integer.parseInt(it.next());
179                assertTrue("Checking for monotonically-increasing list", n > lastP1);
180                lastP1 = n;
181                }
182            // Do the generic tests with current setup.
183            testPBContract(pbn);
184            }
185    
186        /**Test the PaginationBeanTree.
187         */
188        public void testTreeFilterBean()
189            {
190            // NOW TEST ON SOME REALISTIC DATA.
191            // Short names of the exhibits.
192            final String SHNAME1 = "cat-sat-on-mat-ANON.htxt";
193            final String SHNAME2 = "CAT-fat-DHD.jpg"; // Note different case of main word.
194            final String SHNAME3 = "dog-in-manger-1-ANON.gif";
195            final String SHNAME4 = "dog-in-manger-2-ANON.gif";
196            final String SHNAME5 = "Zambia-1-ANON.gif";
197    
198            // Full names of exhibits.
199            final String EXNAME1 = "memes/" + SHNAME1;
200            final String EXNAME2 = "memes/" + SHNAME2;
201            final String EXNAME3 = "other/" + SHNAME3;
202            final String EXNAME4 = "another/" + SHNAME4;
203            final String EXNAME5 = "art/" + SHNAME5;
204    
205            // For now, ignore the effects of attribute words...
206            final List<Name.ExhibitFull> exhibits1 = new ArrayList<Name.ExhibitFull>();
207            exhibits1.add(Name.ExhibitFull.create(EXNAME1));
208            exhibits1.add(Name.ExhibitFull.create(EXNAME2));
209            exhibits1.add(Name.ExhibitFull.create(EXNAME3));
210            exhibits1.add(Name.ExhibitFull.create(EXNAME4));
211            exhibits1.add(Name.ExhibitFull.create(EXNAME5));
212            // Add lots of very similar exhibits for same node that will need lots of pages. */
213            for(int i = 3 + 2 * WebConsts.MAX_RESULTS_PAGES * WebConsts.MAX_RESULTS_PER_PAGE; --i >= 0; )
214                {
215                final String name = "yetanother/dog-in-mangerette-" + i + "-ANON.jpg";
216                exhibits1.add(Name.ExhibitFull.create(name));
217                }
218    
219            // Build this Map non-optimised and optimised to check behaviour.
220            for(final boolean optimised : new boolean[]{ false, true })
221                {
222                // First just create an instance of the bean.
223                final PaginationBeanTree pbt = new PaginationBeanTree();
224    
225                // Test initial values.
226                assertTrue("default number of items should be zero", pbt.getNumberOfItems() == 0);
227                assertTrue("default page number should be 1", pbt.getPg() == 1);
228                // Do the generic tests with current setup.
229                testPBContract(pbt);
230    
231                // Make sure that we can't call setNumberOfItems() directly.
232                try {
233                    pbt.setNumberOfItems(43);
234                    assertTrue("Should not have been able to call setNumberOfItems()", false);
235                    }
236                catch(final Error e) { } // OK: wanted this call vetoed.
237    
238                // Make sure that things work with an empty List.
239                final List<CharSequence> emptyList = Collections.emptyList();
240                pbt.setTreeNodeDetails(Name.EMPTY, emptyList);
241                assertTrue("An empty list should mean no items", pbt.getNumberOfItems() == 0);
242    
243    
244                // Build this map with List nodes.
245                final boolean asSet = false;
246                final Map<Name, Collection<CharSequence>> m1 = TreeFilterBean.computePrefixMap(Collections.unmodifiableList(exhibits1), asSet, optimised);
247    
248                // Extract the root node (which is small enough to get on one page).
249                final CharSequence rootPrefix = Name.EMPTY;
250                final Collection<CharSequence> prefixesAndShortExhibitName = m1.get(rootPrefix);
251                assertTrue(prefixesAndShortExhibitName instanceof List);
252                final List<CharSequence> prefixesAndShortExhibitNameL = (List<CharSequence>) prefixesAndShortExhibitName;
253                pbt.setTreeNodeDetails(rootPrefix,  prefixesAndShortExhibitNameL);
254                // Note that more items are brought down to the root when optimising...
255                assertEquals("Expect small root node (2 items)", optimised ? 5 : 3, pbt.getNumberOfItems());
256                assertEquals("Expect small root node (1 page)", 1, pbt.getNumberOfPages());
257                // Do the generic tests with current setup.
258                testPBContract(pbt);
259    System.out.println(prefixesAndShortExhibitNameL);
260                // Test root nodes: will vary with optimisation.
261                if(!optimised)
262                    {
263                    assertEquals("cat-", prefixesAndShortExhibitNameL.get(0).toString());
264                    assertEquals("dog-", prefixesAndShortExhibitNameL.get(1).toString());
265                    assertEquals("zambia-", prefixesAndShortExhibitNameL.get(2).toString());
266                    }
267                else
268                    {
269                    assertEquals(SHNAME2, prefixesAndShortExhibitNameL.get(0).toString());
270                    assertEquals(SHNAME1, prefixesAndShortExhibitNameL.get(1).toString());
271                    assertEquals("dog-in-manger-", prefixesAndShortExhibitNameL.get(2).toString());
272                    assertEquals("dog-in-mangerette-", prefixesAndShortExhibitNameL.get(3).toString());
273                    assertEquals(SHNAME5, prefixesAndShortExhibitNameL.get(4).toString());
274                    }
275    
276                // Extract the "dog-in-mangerette-" node, which should be huge.
277                final Name dimPrefix = Name.create("dog-in-mangerette-");
278                final Collection<CharSequence> prefixesAndShortExhibitName2 = m1.get(dimPrefix);
279                assertTrue(prefixesAndShortExhibitName2 instanceof List);
280                pbt.setTreeNodeDetails(dimPrefix, (List<CharSequence>) prefixesAndShortExhibitName2);
281                assertTrue("Expect huge root node",
282                           pbt.getNumberOfItems() > WebConsts.MAX_RESULTS_PAGES * WebConsts.MAX_RESULTS_PER_PAGE);
283                // Do the generic tests with current setup.
284                testPBContract(pbt);
285    
286                // Check entries in "cat-" node have correct position (eg sort order)
287                // in spite of difference in case which might reverse things.
288                final Name catPrefix = Name.create("cat-");
289                final Collection<CharSequence> prefixesAndShortExhibitName3 = m1.get(catPrefix);
290                assertTrue(prefixesAndShortExhibitName3 instanceof List);
291                pbt.setTreeNodeDetails(catPrefix, (List<CharSequence>) prefixesAndShortExhibitName3);
292                // Do the generic tests with current setup.
293                testPBContract(pbt);
294                // Test initial elements for correctness...
295                assertEquals(optimised ? SHNAME2 : "cat-fat-", ((List<CharSequence>) prefixesAndShortExhibitName3).get(0).toString());
296                assertEquals(optimised ? SHNAME1 : "cat-sat-", ((List<CharSequence>) prefixesAndShortExhibitName3).get(1).toString());
297                }
298            }
299    
300        /**Test the PaginationBeanTree on a realistic data set from a recent AEP.
301         * This is mainly a test that nothing breaks,
302         * and a sneaky performance test.
303         */
304        public void testTreeFilterBeanOnFullDataSet() throws Exception
305            {
306            // Test performance on a realistic exhibit-name data set.
307            final File lastAEPFile = new File(BackCompatTest.frozenAEPs.get(BackCompatTest.frozenAEPs.size()-1));
308    
309            Main.getOut().println("Using captured AEP " + lastAEPFile);
310            final AllExhibitProperties lastAEP =
311                (AllExhibitProperties) FileTools.deserialiseFromFile(lastAEPFile, true);
312    
313            // Compute as for the 'all exhibits' browsable tree...
314            final List<ExhibitFull> allExhibitNames = lastAEP.aeid.getAllExhibitNamesSorted();
315            final Map<Name, Collection<CharSequence>> prefixMap;
316    
317            // Profile total cost of building a big tree...
318            final ConcurrentMap<StackTraceElement, AtomicInteger> perfCounts = new ConcurrentHashMap<StackTraceElement, AtomicInteger>(1001);
319            final ConcurrentMap<StackTraceElement, ConcurrentMap<StackTraceElement, AtomicInteger>> parentPerfCounts = new ConcurrentHashMap<StackTraceElement, ConcurrentMap<StackTraceElement, AtomicInteger>>(1001);
320            final Thread perfMonitorThread = GenUtils.startThreadPerfMonitor(
321                Thread.currentThread(),
322                perfCounts,
323                parentPerfCounts,
324                "org.hd.", // Capture our code.
325                System.currentTimeMillis() + 1000000, // Monitor thread for at most 1000s.
326                50); // Sample relatively slowly, esp for WinTel development machine.
327            try
328                {
329                prefixMap = TreeFilterBean.computePrefixMap(allExhibitNames, false, true);
330                assertTrue(prefixMap.size() >= allExhibitNames.size());
331                }
332            finally
333                {
334                GenUtils.stopPerfMonitorandDumpSamples(
335                        perfMonitorThread,
336                        "TreeFilterBean all-exhibits profile",
337                        perfCounts,
338                        parentPerfCounts,
339                        20,
340                        GenUtils.systemOutLogger);
341                }
342    
343            // Check that we don't break the pagination bean on this (big) data set...
344            // Extract the root node.
345            final CharSequence rootPrefix = Name.EMPTY;
346            final Collection<CharSequence> prefixesAndShortExhibitName = prefixMap.get(rootPrefix);
347            assertTrue(prefixesAndShortExhibitName instanceof List);
348            final List<CharSequence> prefixesAndShortExhibitNameL = (List<CharSequence>) prefixesAndShortExhibitName;
349            final PaginationBeanTree pbt = new PaginationBeanTree();
350            pbt.setTreeNodeDetails(rootPrefix,  prefixesAndShortExhibitNameL);
351            // Note that more items are brought down to the root when optimising...
352            // Do the generic tests with current setup.
353            testPBContract(pbt);
354            }
355    
356        /**If true, we are very strict and insist that we must use all available buttons.
357         * This is to give users maximum number of useful choices.
358         */
359        private static final boolean MUST_USE_ALL_AVAILABLE_BUTTONS = true;
360    
361        /**Do some tests of generic contract that should work on any PaginationBaseBean or sub-class.
362         * The bean must be passed in already set up.
363         * <p>
364         * This may alter some or all of the bean state.
365         */
366        private static void testPBContract(final PaginationBeanBase pbb)
367            {
368            // Test labels of bean in whatever state it arrived...
369            testLabels(pbb);
370    
371            // For every possible page value, test label generation and parsing...
372            for(int pg = pbb.getNumberOfPages() + 1; --pg > 0; )
373                {
374                // Test that we can always correctly parse every page label and set the page.
375                for(int j =  pbb.getNumberOfPages() + 1; --j > 0; )
376                    {
377                    assertTrue("must always be able to generate and parse every page label", j == pbb.getPageNumber(pbb.getPageLabel(j)));
378                    pbb.setPg(j);
379                    assertTrue("must always be able to set any page from a label", j == pbb.getPg());
380                    }
381    
382                pbb.setPg(pg);
383                assertTrue("Must be able to set current page number", pg == pbb.getPg());
384    
385    //System.out.println("labels..."); for(final Iterator it = pbb.getPageLabels().iterator(); it.hasNext(); ) { System.out.println(it.next()); }
386    
387                // Test that all the page-button labels represent pages in monotonically-increasing order,
388                // and produce the same labels as getPageLabel() for each individual page.
389                int last = 0;
390                final int pageNumbers[] = pbb.getPageNumbers();
391                final List<String> theseLabels = pbb.getPageLabels();
392                final int lastPageIndex = Math.max(1, pbb.getNumberOfPages());
393                assertTrue("labels and numbers must be same length", pageNumbers.length == theseLabels.size());
394    //System.out.print("Page sequence (current page = "+pg+"):");
395                for(int i = 0; i < pageNumbers.length; ++i)
396                    {
397                    // Test labels of bean in every possible bean state...
398                    testLabels(pbb);
399    
400                    final int pgNum = pageNumbers[i];
401    //System.out.print(" " + pgNum);
402                    assertTrue("all displayed-page numbers must be in range", (pgNum >= 1) && (pgNum <= lastPageIndex));
403                    assertTrue("labels must represent monotonically-ascending pages", pgNum > last);
404                    final String label = pbb.getPageLabel(pgNum);
405                    assertTrue("getPageNumber() must be the inverse of getPageLabel()", pgNum == pbb.getPageNumber(label));
406                    final String s = theseLabels.get(i);
407                    assertTrue("label set must match individual labels (pg "+pgNum+"): "+s+"/"+label, s.equals(label));
408                    last = pgNum;
409                    }
410    //System.out.println(" ...end ("+theseLabels.size()+" buttons).");
411                }
412            }
413    
414        /**Test some characteristics of the labels obtained from the bean.
415         */
416        private static void testLabels(final PaginationBeanBase pbb)
417            {
418            final List<String> labels = pbb.getPageLabels();
419    
420            // Under all circumstances, number of pages and items must be non-negative.
421            assertTrue("Number of items and pages must be non-negative",
422                       (pbb.getNumberOfItems() >= 0) && (pbb.getNumberOfPages() >= 0));
423            // The page number must be suitably constrained.
424            assertTrue("Page number must be [1, numberOfPages] if more than zero items: " + pbb.getPg() + " [" + pbb.getNumberOfPages() + "]",
425                       (pbb.getPg() >= 1) &&
426                            ((pbb.getNumberOfItems() == 0) || (pbb.getPg() <= pbb.getNumberOfPages())));
427            // Under all circumstances, the number of page buttons must be no more than WebConsts.MAX_RESULTS_PAGES.
428            assertTrue("Must not produce too many pages", labels.size() <= WebConsts.MAX_RESULTS_PAGES);
429    
430            // We also ensure (though this is in principle less critical)
431            // that at least a substantial fraction of the possible labels are used,
432            // else the user may have to click too many buttons.
433            assertTrue("A reasonable fraction of available buttons must be used",
434                       labels.size() >= Math.min(WebConsts.MAX_RESULTS_PAGES, pbb.getNumberOfPages()) / 2);
435    
436            // If being strict we insist that all buttons are used.
437            if(MUST_USE_ALL_AVAILABLE_BUTTONS)
438                {
439                assertTrue("All available buttons must be used so as to give the user the shortest path to their target page",
440                           labels.size() == Math.min(WebConsts.MAX_RESULTS_PAGES, pbb.getNumberOfPages()));
441                }
442    
443            // Test that all the page-button labels are unique.
444            assertTrue("All labels must be unique", (new HashSet<String>(labels)).size() == labels.size());
445            assertTrue("labels and numbers must be same length", pbb.getPageNumbers().length == labels.size());
446            }
447        }