1 /*
2  * JasperReports - Free Java Reporting Library.
3  * Copyright (C) 2001 - 2019 TIBCO Software Inc. All rights reserved.
4  * http://www.jaspersoft.com
5  *
6  * Unless you have purchased a commercial license agreement from Jaspersoft,
7  * the following license terms apply:
8  *
9  * This program is part of JasperReports.
10  *
11  * JasperReports is free software: you can redistribute it and/or modify
12  * it under the terms of the GNU Lesser General Public License as published by
13  * the Free Software Foundation, either version 3 of the License, or
14  * (at your option) any later version.
15  *
16  * JasperReports is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19  * GNU Lesser General Public License for more details.
20  *
21  * You should have received a copy of the GNU Lesser General Public License
22  * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
23  */

24 package net.sf.jasperreports.engine.fill;
25
26 import java.sql.Connection;
27 import java.util.ArrayList;
28 import java.util.HashMap;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Map;
33 import java.util.TimeZone;
34
35 import org.apache.commons.javaflow.api.continuable;
36 import org.apache.commons.logging.Log;
37 import org.apache.commons.logging.LogFactory;
38
39 import net.sf.jasperreports.engine.BookmarkHelper;
40 import net.sf.jasperreports.engine.JRAbstractScriptlet;
41 import net.sf.jasperreports.engine.JRDataSource;
42 import net.sf.jasperreports.engine.JRDataset;
43 import net.sf.jasperreports.engine.JRException;
44 import net.sf.jasperreports.engine.JRExpression;
45 import net.sf.jasperreports.engine.JRParameter;
46 import net.sf.jasperreports.engine.JRPrintElement;
47 import net.sf.jasperreports.engine.JRPrintPage;
48 import net.sf.jasperreports.engine.JRPropertiesUtil;
49 import net.sf.jasperreports.engine.JRRuntimeException;
50 import net.sf.jasperreports.engine.JRVirtualizer;
51 import net.sf.jasperreports.engine.JasperPrint;
52 import net.sf.jasperreports.engine.JasperReport;
53 import net.sf.jasperreports.engine.JasperReportsContext;
54 import net.sf.jasperreports.engine.ReportContext;
55 import net.sf.jasperreports.engine.base.JRVirtualPrintPage;
56 import net.sf.jasperreports.engine.type.CalculationEnum;
57 import net.sf.jasperreports.engine.util.DefaultFormatFactory;
58 import net.sf.jasperreports.engine.util.FormatFactory;
59 import net.sf.jasperreports.engine.util.JRGraphEnvInitializer;
60 import net.sf.jasperreports.repo.RepositoryContext;
61 import net.sf.jasperreports.repo.SimpleRepositoryContext;
62
63 /**
64  * @author Teodor Danciu (teodord@users.sourceforge.net)
65  */

66 public abstract class BaseReportFiller implements ReportFiller
67 {
68     private static final Log log = LogFactory.getLog(BaseReportFiller.class);
69     
70     protected JasperReportsContext jasperReportsContext;
71     protected JRPropertiesUtil propertiesUtil;
72
73     protected JRFillContext fillContext;
74     
75     protected FillerParent parent;
76     
77     protected final int fillerId;
78
79     protected List<String> printTransferPropertyPrefixes;
80
81     protected JasperReportSource reportSource;
82     
83     /**
84      * The report.
85      */

86     protected JasperReport jasperReport;
87     
88     protected RepositoryContext repositoryContext;
89
90     protected JRCalculator calculator;
91
92     protected final JRFillObjectFactory factory;
93
94     /**
95      * Main report dataset.
96      */

97     protected JRFillDataset mainDataset;
98
99     /**
100      * Map of datasets ({@link JRFillDataset JRFillDataset} objects} indexed by name.
101      */

102     protected Map<String,JRFillDataset> datasetMap;
103
104     protected DelayedFillActions delayedActions;
105
106     protected JRAbstractScriptlet scriptlet;
107
108     protected FormatFactory formatFactory;
109     
110     protected boolean ignorePagination;
111     
112     protected BookmarkHelper bookmarkHelper;
113     
114     protected JRVirtualizationContext virtualizationContext;
115     
116     protected JasperPrint jasperPrint;
117     
118     protected Thread fillingThread;
119     
120     private boolean isInterrupted;
121     private boolean threadInterrupted;
122
123     protected FillListener fillListener;
124     
125     protected int usedPageWidth = 0;
126
127     public BaseReportFiller(JasperReportsContext jasperReportsContext, JasperReport jasperReport, 
128             FillerParent parent) throws JRException
129     {
130         this(jasperReportsContext, SimpleJasperReportSource.from(jasperReport), parent);
131     }
132
133     public BaseReportFiller(JasperReportsContext jasperReportsContext, JasperReportSource reportSource, 
134             FillerParent parent) throws JRException
135     {
136         JRGraphEnvInitializer.initializeGraphEnv();
137         
138         setJasperReportsContext(jasperReportsContext);
139         
140         this.reportSource = reportSource;
141         this.jasperReport = reportSource.getReport();
142         this.repositoryContext = SimpleRepositoryContext.of(jasperReportsContext, reportSource.getRepositoryReportContext());
143         jasperReportSet();
144         
145         this.parent = parent;
146
147         DatasetExpressionEvaluator initEvaluator = null;
148         if (parent == null)
149         {
150             fillContext = new JRFillContext(this);
151             printTransferPropertyPrefixes = readPrintTransferPropertyPrefixes();
152         }
153         else
154         {
155             fillContext = parent.getFiller().fillContext;
156             printTransferPropertyPrefixes = parent.getFiller().printTransferPropertyPrefixes;
157             initEvaluator = parent.getCachedEvaluator();
158         }
159         
160         this.fillerId = fillContext.generatedFillerId();
161         if (log.isDebugEnabled())
162         {
163             log.debug("Fill " + fillerId + ": created for " + jasperReport.getName());
164         }
165         
166         if (initEvaluator == null)
167         {
168             calculator = JRFillDataset.createCalculator(jasperReportsContext, jasperReport, jasperReport.getMainDataset());
169         }
170         else
171         {
172             calculator = new JRCalculator(initEvaluator);
173         }
174         
175         jasperPrint = new JasperPrint();
176         
177         factory = initFillFactory();
178
179         createDatasets();
180         mainDataset = factory.getDataset(jasperReport.getMainDataset());
181         
182         if (parent == null)
183         {
184             FillDatasetPosition masterFillPosition = new FillDatasetPosition(null);
185             mainDataset.setFillPosition(masterFillPosition);
186         }
187
188         delayedActions = new DelayedFillActions(this);
189         if (log.isDebugEnabled())
190         {
191             log.debug("created delayed actions " + delayedActions.getId() + for filler " + fillerId);
192         }
193     }
194     
195     protected abstract void jasperReportSet();
196     
197     private List<String> readPrintTransferPropertyPrefixes()
198     {
199         List<JRPropertiesUtil.PropertySuffix> transferProperties = propertiesUtil.getProperties(
200                 JasperPrint.PROPERTIES_PRINT_TRANSFER_PREFIX);
201         List<String> prefixes = new ArrayList<String>(transferProperties.size());
202         for (JRPropertiesUtil.PropertySuffix property : transferProperties)
203         {
204             String transferPrefix = property.getValue();
205             if (transferPrefix != null && transferPrefix.length() > 0)
206             {
207                 prefixes.add(transferPrefix);
208             }
209         }
210         return prefixes;
211     }
212
213     protected abstract JRFillObjectFactory initFillFactory();
214
215     private void createDatasets() throws JRException
216     {
217         datasetMap = new HashMap<String,JRFillDataset>();
218
219         JRDataset[] datasets = jasperReport.getDatasets();
220         if (datasets != null && datasets.length > 0)
221         {
222             for (int i = 0; i < datasets.length; i++)
223             {
224                 JRFillDataset fillDataset = factory.getDataset(datasets[i]);
225                 fillDataset.createCalculator(jasperReport);
226
227                 datasetMap.put(datasets[i].getName(), fillDataset);
228             }
229         }
230     }
231
232     protected final void initDatasets() throws JRException
233     {
234         mainDataset.initElementDatasets(factory);
235         initDatasets(factory);
236
237         mainDataset.checkVariableCalculationReqs(factory);
238
239         mainDataset.setCalculator(calculator);
240         mainDataset.initCalculator();
241     }
242
243     private void initDatasets(JRFillObjectFactory factory)
244     {
245         for (Iterator<JRFillDataset> it = datasetMap.values().iterator(); it.hasNext();)
246         {
247             JRFillDataset dataset = it.next();
248             dataset.inheritFromMain();
249             dataset.initElementDatasets(factory);
250         }
251     }
252
253     protected final void createBoundElementMaps(JREvaluationTime evaluationTime)
254     {
255         delayedActions.createDelayedEvaluationTime(evaluationTime);
256     }
257     
258     /**
259      * Adds a fill lister to be notified by events that occur during the fill.
260      * 
261      * @param fillListener the listener to add
262      */

263     @Override
264     public void addFillListener(FillListener fillListener)
265     {
266         this.fillListener = CompositeFillListener.addListener(this.fillListener, fillListener);
267     }
268
269     public JasperReportsContext getJasperReportsContext()
270     {
271         return jasperReportsContext;
272     }
273     
274     public RepositoryContext getRepositoryContext()
275     {
276         return repositoryContext;
277     }
278
279     public JRPropertiesUtil getPropertiesUtil()
280     {
281         return propertiesUtil;
282     }
283
284     public JasperReportSource getReportSource()
285     {
286         return reportSource;
287     }
288
289     /**
290      * Returns the report.
291      *
292      * @return the report
293      */

294     public JasperReport getJasperReport()
295     {
296         return jasperReport;
297     }
298
299     public JasperPrint getJasperPrint()
300     {
301         return jasperPrint;
302     }
303
304     protected void setJasperReportsContext(JasperReportsContext jasperReportsContext)
305     {
306         this.jasperReportsContext = jasperReportsContext;
307         this.propertiesUtil = JRPropertiesUtil.getInstance(jasperReportsContext);
308     }
309
310     protected final void setParametersToContext(Map<String,Object> parameterValues)
311     {
312         @SuppressWarnings("deprecation")
313         JasperReportsContext localContext = 
314             net.sf.jasperreports.engine.util.LocalJasperReportsContext.getLocalContext(jasperReportsContext, parameterValues);
315         if (localContext != jasperReportsContext)
316         {
317             setJasperReportsContext(localContext);
318         }
319     }
320
321     protected void initVirtualizationContext(Map<String, Object> parameterValues)
322     {
323         if (isSubreport())
324         {
325             if (fillContext.isUsingVirtualizer())
326             {
327                 if (parent.isParentPagination())// using this method to tell between part and band parents 
328                 {
329                     // this is a filler of a subreport in a band parent, creating a subcontext for the subreport.
330                     // this allows setting a separate listener, and guarantees that
331                     // the current subreport page is not externalized.
332                     virtualizationContext = new JRVirtualizationContext(fillContext.getVirtualizationContext());//FIXME lucianc clear this context from the virtualizer
333                     
334                     // setting per subreport page size
335                     setVirtualPageSize(parameterValues);
336                 }
337                 else
338                 {
339                     // the parent is a part filler, using the master virtualization context
340                     //FIXMEBOOK JRVirtualPrintPage.PROPERTY_VIRTUAL_PAGE_ELEMENT_SIZE at part level is not used
341                     virtualizationContext = fillContext.getVirtualizationContext();
342                 }
343             }
344         }
345         else
346         {
347             /* Virtualizer */
348             JRVirtualizer virtualizer = (JRVirtualizer) parameterValues.get(JRParameter.REPORT_VIRTUALIZER);
349             if (virtualizer == null)
350             {
351                 return;
352             }
353             
354             if (log.isDebugEnabled())
355             {
356                 log.debug("Fill " + fillerId + ": using virtualizer " + virtualizer);
357             }
358
359             fillContext.setUsingVirtualizer(true);
360             
361             virtualizationContext = fillContext.getVirtualizationContext();
362             virtualizationContext.setVirtualizer(virtualizer);
363             
364             setVirtualPageSize(parameterValues);
365             
366             JRVirtualizationContext.register(virtualizationContext, jasperPrint);
367         }
368         
369         if (virtualizationContext != null && log.isDebugEnabled())
370         {
371             log.debug("filler " + fillerId + " created virtualization context " + virtualizationContext);
372         }
373     }
374
375     protected void setVirtualPageSize(Map<String, Object> parameterValues)
376     {
377         // see if we have a parameter for the page size
378         Integer virtualPageSize = (Integer) parameterValues.get(
379                 JRVirtualPrintPage.PROPERTY_VIRTUAL_PAGE_ELEMENT_SIZE);
380         if (virtualPageSize == null)
381         {
382             // check if we have a property
383             String pageSizeProp = jasperReport.getPropertiesMap().getProperty(
384                     JRVirtualPrintPage.PROPERTY_VIRTUAL_PAGE_ELEMENT_SIZE);
385             if (pageSizeProp != null)
386             {
387                 virtualPageSize = JRPropertiesUtil.asInteger(pageSizeProp);
388             }
389         }
390         
391         if (virtualPageSize != null)
392         {
393             if (log.isDebugEnabled())
394             {
395                 log.debug("virtual page size " + virtualPageSize);
396             }
397             
398             // override the default
399             virtualizationContext.setPageElementSize(virtualPageSize);
400         }
401     }
402
403     @Override
404     @continuable
405     public JasperPrint fill(Map<String,Object> parameterValues, Connection conn) throws JRException
406     {
407         if (parameterValues == null)
408         {
409             parameterValues = new HashMap<String,Object>();
410         }
411
412         setConnectionParameterValue(parameterValues, conn);
413
414         return fill(parameterValues);
415     }
416
417     protected void setConnectionParameterValue(Map<String,Object> parameterValues, Connection conn)
418     {
419         mainDataset.setConnectionParameterValue(parameterValues, conn);
420     }
421
422     @Override
423     @continuable
424     public JasperPrint fill(Map<String,Object> parameterValues, JRDataSource ds) throws JRException
425     {
426         if (parameterValues == null)
427         {
428             parameterValues = new HashMap<String,Object>();
429         }
430
431         setDatasourceParameterValue(parameterValues, ds);
432
433         return fill(parameterValues);
434     }
435
436     protected void setDatasourceParameterValue(Map<String,Object> parameterValues, JRDataSource ds)
437     {
438         mainDataset.setDatasourceParameterValue(parameterValues, ds);
439     }
440     
441     protected void setParameters(Map<String,Object> parameterValues) throws JRException
442     {
443         initVirtualizationContext(parameterValues);
444
445         setFormatFactory(parameterValues);
446
447         setIgnorePagination(parameterValues);
448
449         if (parent == null)
450         {
451             ReportContext reportContext = (ReportContext) parameterValues.get(JRParameter.REPORT_CONTEXT);
452             fillContext.setReportContext(reportContext);
453         }
454
455         mainDataset.setParameterValues(parameterValues);
456         mainDataset.evaluateFieldProperties();
457         mainDataset.initDatasource();
458
459         this.scriptlet = mainDataset.delegateScriptlet;
460
461         if (!isSubreport())
462         {
463             fillContext.setMasterFormatFactory(getFormatFactory());
464             fillContext.setMasterLocale(getLocale());
465             fillContext.setMasterTimeZone(getTimeZone());
466         }
467     }
468     
469     protected void setBookmarkHelper()
470     {
471         boolean isCreateBookmarks = propertiesUtil.getBooleanProperty(mainDataset, 
472                 JasperPrint.PROPERTY_CREATE_BOOKMARKS, false);
473         if (isCreateBookmarks)
474         {
475             boolean collapseMissingLevels = propertiesUtil.getBooleanProperty(mainDataset, 
476                 JasperPrint.PROPERTY_COLLAPSE_MISSING_BOOKMARK_LEVELS, false);
477             bookmarkHelper = new BookmarkHelper(collapseMissingLevels);
478         }
479     }
480
481     protected void setIgnorePagination(Map<String,Object> parameterValues)
482     {
483         boolean ignore;
484         if (parent == null)
485         {
486             ignore = getOwnIgnorePagination(parameterValues, false);
487         }
488         else
489         {
490             if (parent.isParentPagination())
491             {
492                 // the parent drives pagination, not looking at parameter and flag
493                 ignore = parent.getFiller().ignorePagination;
494             }
495             else
496             {
497                 // subreport parts are allowed to ignore pagination even if the parent does not have the flag
498                 // the report attribute overrides the parent only when set to true
499                 Boolean ownIgnorePagination = getOwnIgnorePagination(parameterValues, true);
500                 if (ownIgnorePagination != null)
501                 {
502                     ignore = ownIgnorePagination;
503                 }
504                 else
505                 {
506                     // default to the parent
507                     ignore = parent.getFiller().ignorePagination;
508                 }
509             }
510         }
511         
512         ignorePagination = ignore;
513         parameterValues.put(JRParameter.IS_IGNORE_PAGINATION, ignorePagination);
514         ignorePaginationSet(parameterValues);
515     }
516     
517     protected Boolean getOwnIgnorePagination(Map<String,Object> parameterValues, boolean onlySetAttribute)
518     {
519         Boolean isIgnorePaginationParam = (Boolean) parameterValues.get(JRParameter.IS_IGNORE_PAGINATION);
520         if (isIgnorePaginationParam != null)
521         {
522             return isIgnorePaginationParam;
523         }
524         
525         boolean ignorePaginationAttribute = jasperReport.isIgnorePagination();
526         if (ignorePaginationAttribute)
527         {
528             return ignorePaginationAttribute;
529         }
530         
531         return onlySetAttribute ? null : false;
532     }
533     
534     protected abstract void ignorePaginationSet(Map<String, Object> parameterValues);
535     
536     public boolean isIgnorePagination()
537     {
538         return ignorePagination;
539     }
540     
541     protected boolean isInterrupted()
542     {
543         return (isInterrupted || threadInterrupted || (parent != null && parent.getFiller().isInterrupted()));
544     }
545     
546     protected boolean isDeliberatelyInterrupted()
547     {
548         return (isInterrupted || (parent != null && parent.getFiller().isDeliberatelyInterrupted()));
549     }
550
551     protected void setInterrupted(boolean isInterrupted)
552     {
553         this.isInterrupted = isInterrupted;
554     }
555
556     protected void checkInterrupted()
557     {
558         if (Thread.interrupted())
559         {
560             threadInterrupted = true;
561         }
562         
563         if (isInterrupted())
564         {
565             if (log.isDebugEnabled())
566             {
567                 log.debug("Fill " + fillerId + ": interrupting");
568             }
569
570             throw new JRFillInterruptedException();
571         }
572     }
573
574     @Override
575     public JRFillContext getFillContext()
576     {
577         return fillContext;
578     }
579
580     public JRVirtualizationContext getVirtualizationContext()
581     {
582         return virtualizationContext;
583     }
584
585     public JRFillDataset getMainDataset()
586     {
587         return mainDataset;
588     }
589     
590     /**
591      * Returns the map of parameter values.
592      * 
593      * @return the map of parameter values
594      */

595     public Map<String,Object> getParameterValuesMap()
596     {
597         return mainDataset.getParameterValuesMap();
598     }
599
600     /**
601      * Returns the report parameters indexed by name.
602      *
603      * @return the report parameters map
604      */

605     protected Map<String,JRFillParameter> getParametersMap()
606     {
607         return mainDataset.parametersMap;
608     }
609     
610     /**
611      * Returns the value of a parameter.
612      * 
613      * @param parameterName the parameter name
614      * @return the parameter value
615      */

616     public Object getParameterValue(String parameterName)
617     {
618         return mainDataset.getParameterValue(parameterName);
619     }
620
621     /**
622      * Returns the report locale.
623      *
624      * @return the report locale
625      */

626     protected Locale getLocale()
627     {
628         return mainDataset.getLocale();
629     }
630
631     /**
632      * Returns the report time zone.
633      *
634      * @return the report time zone
635      */

636     protected TimeZone getTimeZone()
637     {
638         return mainDataset.timeZone;
639     }
640
641     /**
642      * Adds a variable calculation request.
643      *
644      * @param variableName
645      *            the variable name
646      * @param calculation
647      *            the calculation type
648      */

649     protected void addVariableCalculationReq(String variableName, CalculationEnum calculation)
650     {
651         mainDataset.addVariableCalculationReq(variableName, calculation);
652     }
653
654     /**
655      * Returns a report variable.
656      *
657      * @param variableName the variable name
658      * @return the variable
659      */

660     public JRFillVariable getVariable(String variableName)
661     {
662         return mainDataset.getVariable(variableName);
663     }
664
665     /**
666      * Returns the value of a variable.
667      *
668      * @param variableName
669      *            the variable name
670      *
671      * @return the variable value
672      *
673      * @throws JRRuntimeException when the variable does not exist
674      */

675     public Object getVariableValue(String variableName)
676     {
677         return mainDataset.getVariableValue(variableName);
678     }
679
680     protected JRFillExpressionEvaluator getExpressionEvaluator()
681     {
682         return calculator;
683     }
684
685     protected boolean isSubreport()
686     {
687         return parent != null;
688     }
689
690     protected boolean isMasterReport()
691     {
692         return parent == null;
693     }
694
695     /**
696      * Evaluates an expression
697      * @param expression the expression
698      * @param evaluation the evaluation type
699      * @return the evaluation result
700      * @throws JRException
701      */

702     public Object evaluateExpression(JRExpression expression, byte evaluation) throws JRException
703     {
704         return mainDataset.evaluateExpression(expression, evaluation);
705     }
706
707     protected final void setFormatFactory(Map<String,Object> parameterValues)
708     {
709         formatFactory = (FormatFactory)parameterValues.get(JRParameter.REPORT_FORMAT_FACTORY);
710         if (formatFactory == null)
711         {
712             formatFactory = DefaultFormatFactory.createFormatFactory(jasperReport.getFormatFactoryClass());
713             parameterValues.put(JRParameter.REPORT_FORMAT_FACTORY, formatFactory);
714         }
715     }
716
717     /**
718      * Returns the report format factory.
719      *
720      * @return the report format factory
721      */

722     protected FormatFactory getFormatFactory()
723     {
724         return formatFactory;
725     }
726
727     protected void addLastPageBookmarks()
728     {
729         if (bookmarkHelper != null)
730         {
731             int pageIndex = jasperPrint.getPages() == null ? -1 : (jasperPrint.getPages().size() - 1);
732             if (pageIndex >= 0)
733             {
734                 JRPrintPage page = jasperPrint.getPages().get(pageIndex);
735                 bookmarkHelper.addBookmarks(page, pageIndex);
736             }
737         }
738     }
739
740     public void updateBookmark(JRPrintElement element)
741     {
742         if (isMasterReport())
743         {
744             if (bookmarkHelper != null)
745             {
746                 bookmarkHelper.updateBookmark(element);
747             }
748         }
749         else
750         {
751             parent.updateBookmark(element);
752         }
753     }
754
755
756     /**
757      * Cancels the fill process.
758      *
759      * @throws JRException
760      */

761     @Override
762     public void cancelFill() throws JRException
763     {
764         if (log.isDebugEnabled())
765         {
766             log.debug("Fill " + fillerId + ": cancelling");
767         }
768
769         fillContext.markCanceled();
770         
771         if (fillContext.cancelRunningQuery())
772         {
773             if (log.isDebugEnabled())
774             {
775                 log.debug("Fill " + fillerId + ": query cancelled");
776             }
777         }
778         else
779         {
780             Thread t = fillingThread;
781             if (t != null)
782             {
783                 if (log.isDebugEnabled())
784                 {
785                     log.debug("Fill " + fillerId + ": Interrupting thread " + t);
786                 }
787
788                 t.interrupt();
789             }
790         }
791     }
792
793     protected void addBoundElement(JRFillElement element, JRPrintElement printElement, JREvaluationTime evaluationTime,
794             FillPageKey pageKey)
795     {
796         if (log.isDebugEnabled())
797         {
798             log.debug("Adding evaluation of " + printElement + " by " + element 
799                     + for evaluation " + evaluationTime);
800         }
801         
802         delayedActions.addDelayedAction(element, printElement, evaluationTime, pageKey);
803     }
804
805     protected void resolveBoundElements(JREvaluationTime evaluationTime, byte evaluation) throws JRException
806     {
807         delayedActions.runActions(evaluationTime, evaluation);
808     }
809     
810     protected void resolveMasterBoundElements() throws JRException
811     {
812         resolveBoundElements(JREvaluationTime.EVALUATION_TIME_MASTER, JRExpression.EVALUATION_DEFAULT);
813     }
814     
815     public void recordUsedPageWidth(int width)
816     {
817         if (width > usedPageWidth)
818         {
819             usedPageWidth = width;
820         }
821     }
822     
823     public int getUsedPageWidth()
824     {
825         return usedPageWidth;
826     }
827
828 }