/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2001 - 2016 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.states.process; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.engine.classic.core.AbstractReportDefinition; import org.pentaho.reporting.engine.classic.core.AttributeNames; import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; import org.pentaho.reporting.engine.classic.core.CrosstabGroup; import org.pentaho.reporting.engine.classic.core.DataFactory; import org.pentaho.reporting.engine.classic.core.DataRow; import org.pentaho.reporting.engine.classic.core.Group; import org.pentaho.reporting.engine.classic.core.InvalidReportStateException; import org.pentaho.reporting.engine.classic.core.MasterReport; import org.pentaho.reporting.engine.classic.core.ParameterMapping; import org.pentaho.reporting.engine.classic.core.ReportDefinition; import org.pentaho.reporting.engine.classic.core.ReportElement; import org.pentaho.reporting.engine.classic.core.ReportParameterValidationException; import org.pentaho.reporting.engine.classic.core.ReportProcessingException; import org.pentaho.reporting.engine.classic.core.ResourceBundleFactory; import org.pentaho.reporting.engine.classic.core.RootLevelBand; import org.pentaho.reporting.engine.classic.core.Section; import org.pentaho.reporting.engine.classic.core.SubReport; import org.pentaho.reporting.engine.classic.core.cache.CachingDataFactory; import org.pentaho.reporting.engine.classic.core.event.ReportEvent; import org.pentaho.reporting.engine.classic.core.filter.types.ExternalElementType; import org.pentaho.reporting.engine.classic.core.function.Expression; import org.pentaho.reporting.engine.classic.core.function.ExpressionRuntime; import org.pentaho.reporting.engine.classic.core.function.OutputFunction; import org.pentaho.reporting.engine.classic.core.function.ProcessingContext; import org.pentaho.reporting.engine.classic.core.function.ProcessingDataFactoryContext; import org.pentaho.reporting.engine.classic.core.function.StructureFunction; import org.pentaho.reporting.engine.classic.core.function.sys.AttributeExpressionsEvaluator; import org.pentaho.reporting.engine.classic.core.function.sys.CellFormatFunction; import org.pentaho.reporting.engine.classic.core.function.sys.MetaDataStyleEvaluator; import org.pentaho.reporting.engine.classic.core.function.sys.SheetNameFunction; import org.pentaho.reporting.engine.classic.core.function.sys.StyleExpressionsEvaluator; import org.pentaho.reporting.engine.classic.core.function.sys.StyleResolvingEvaluator; import org.pentaho.reporting.engine.classic.core.function.sys.WizardItemHideFunction; import org.pentaho.reporting.engine.classic.core.layout.InlineSubreportMarker; import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorFeature; import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorMetaData; import org.pentaho.reporting.engine.classic.core.parameters.DefaultParameterContext; import org.pentaho.reporting.engine.classic.core.parameters.ReportParameterDefinition; import org.pentaho.reporting.engine.classic.core.parameters.ReportParameterValidator; import org.pentaho.reporting.engine.classic.core.parameters.ValidationResult; import org.pentaho.reporting.engine.classic.core.sorting.SortConstraint; import org.pentaho.reporting.engine.classic.core.sorting.SortingDataFactory; import org.pentaho.reporting.engine.classic.core.states.DataFactoryManager; import org.pentaho.reporting.engine.classic.core.states.DefaultGroupSizeRecorder; import org.pentaho.reporting.engine.classic.core.states.DefaultGroupingState; import org.pentaho.reporting.engine.classic.core.states.DesignTimeDataFactory; import org.pentaho.reporting.engine.classic.core.states.EmptyDataFactory; import org.pentaho.reporting.engine.classic.core.states.EmptyGroupSizeRecorder; import org.pentaho.reporting.engine.classic.core.states.FunctionStorage; import org.pentaho.reporting.engine.classic.core.states.FunctionStorageKey; import org.pentaho.reporting.engine.classic.core.states.GroupSizeRecorder; import org.pentaho.reporting.engine.classic.core.states.GroupStartRecord; import org.pentaho.reporting.engine.classic.core.states.GroupingState; import org.pentaho.reporting.engine.classic.core.states.IgnoreEverythingReportErrorHandler; import org.pentaho.reporting.engine.classic.core.states.InitialLayoutProcess; import org.pentaho.reporting.engine.classic.core.states.LayoutProcess; import org.pentaho.reporting.engine.classic.core.states.PerformanceMonitorContext; import org.pentaho.reporting.engine.classic.core.states.ProcessStateHandle; import org.pentaho.reporting.engine.classic.core.states.ReportDefinitionImpl; import org.pentaho.reporting.engine.classic.core.states.ReportProcessingErrorHandler; import org.pentaho.reporting.engine.classic.core.states.ReportState; import org.pentaho.reporting.engine.classic.core.states.ReportStateKey; import org.pentaho.reporting.engine.classic.core.states.StateUtilities; import org.pentaho.reporting.engine.classic.core.states.StructureFunctionComparator; import org.pentaho.reporting.engine.classic.core.states.SubLayoutProcess; import org.pentaho.reporting.engine.classic.core.states.SubReportStorage; import org.pentaho.reporting.engine.classic.core.states.crosstab.CrosstabProcessorFunction; import org.pentaho.reporting.engine.classic.core.states.datarow.DefaultFlowController; import org.pentaho.reporting.engine.classic.core.states.datarow.InlineDataRowRuntime; import org.pentaho.reporting.engine.classic.core.states.datarow.MasterDataRow; import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys; import org.pentaho.reporting.engine.classic.core.style.StyleSheet; import org.pentaho.reporting.engine.classic.core.util.IntegerCache; import org.pentaho.reporting.engine.classic.core.util.LongSequence; import org.pentaho.reporting.engine.classic.core.util.ReportParameterValues; import org.pentaho.reporting.engine.classic.core.util.beans.BeanException; import org.pentaho.reporting.engine.classic.core.util.beans.ConverterRegistry; import org.pentaho.reporting.engine.classic.core.wizard.ProxyDataSchemaDefinition; import org.pentaho.reporting.libraries.base.config.Configuration; import org.pentaho.reporting.libraries.base.config.HierarchicalConfiguration; import org.pentaho.reporting.libraries.base.config.ModifiableConfiguration; import org.pentaho.reporting.libraries.base.util.ArgumentNullException; import org.pentaho.reporting.libraries.base.util.FastStack; import org.pentaho.reporting.libraries.base.util.PerformanceLoggingStopWatch; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.EventListenerList; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; @SuppressWarnings( "HardCodedStringLiteral" ) public class ProcessState implements ReportState { private static class InternalProcessHandle implements ProcessStateHandle { private PerformanceMonitorContext monitorContext; private DataFactoryManager manager; private InternalProcessHandle( final DataFactoryManager manager, final PerformanceMonitorContext monitorContext ) { this.manager = manager; this.monitorContext = monitorContext; } public void close() { // close the data-factory manager ... monitorContext.close(); manager.close(); } } private static class InternalPerformanceMonitorContext implements PerformanceMonitorContext { private PerformanceMonitorContext parent; private EventListenerList listeners; private InternalPerformanceMonitorContext( final PerformanceMonitorContext parent ) { this.parent = parent; this.listeners = new EventListenerList(); } public PerformanceLoggingStopWatch createStopWatch( final String tag ) { return parent.createStopWatch( tag ); } public PerformanceLoggingStopWatch createStopWatch( final String tag, final Object message ) { return parent.createStopWatch( tag, message ); } public void addChangeListener( final ChangeListener listener ) { listeners.add( ChangeListener.class, listener ); } public void removeChangeListener( final ChangeListener listener ) { listeners.remove( ChangeListener.class, listener ); } public void close() { ChangeEvent event = new ChangeEvent( this ); for ( final ChangeListener changeListener : listeners.getListeners( ChangeListener.class ) ) { changeListener.stateChanged( event ); } } } public static final int ARTIFICIAL_EVENT_CODE = ReportEvent.ARTIFICIAL_EVENT_CODE; private static final Log logger = LogFactory.getLog( ProcessState.class ); private int currentGroupIndex; private int currentPresentationGroupIndex; private ReportDefinitionImpl report; private int currentSubReport; private InlineSubreportMarker[] subReports; private ProcessState suspendedState; private ProcessState parentSubReportState; private FunctionStorage functionStorage; private FunctionStorage structureFunctionStorage; private DataFactoryManager dataFactoryManager; private InternalProcessHandle processHandle; private DefaultFlowController flowController; private LayoutProcess layoutProcess; private ReportStateKey processKey; private AdvanceHandler advanceHandler; private ReportProcessingErrorHandler errorHandler; private int sequenceCounter; private boolean inItemGroup; private InlineSubreportMarker currentSubReportMarker; private boolean inlineProcess; private FastStack<GroupStartRecord> groupStarts; private boolean structuralPreprocessingNeeded; private HashSet<Integer> processLevels; private SubReportStorage subReportStorage; private String query; private Integer queryLimit; private Integer queryTimeout; private GroupSizeRecorder recorder; private boolean reportInstancesShareConnection; private AdvanceHandler postSummaryRowAdvanceHandler; private int replayStoredCrosstabGroup; private LongSequence groupSequenceCounter; private LongSequence crosstabColumnSequenceCounter; private boolean designtime; private PerformanceMonitorContext performanceMonitorContext; private ReportProcessStore processStore; public ProcessState() { } public boolean initializeForMasterReport( final MasterReport report, final ProcessingContext processingContext, final OutputFunction outputFunction ) throws ReportProcessingException { ArgumentNullException.validate( "report", report ); ArgumentNullException.validate( "processingContext", processingContext ); ArgumentNullException.validate( "outputFunction", outputFunction ); final ReportParameterDefinition parameters = report.getParameterDefinition(); final DefaultParameterContext parameterContext = new DefaultParameterContext( report ); // pre-init the output-processor-metadata. initializeProcessingContext( processingContext, report ); this.designtime = processingContext.getOutputProcessorMetaData().isFeatureSupported( OutputProcessorFeature.DESIGNTIME ); final ReportParameterValues parameterValues; if ( designtime == false ) { try { final ReportParameterValidator reportParameterValidator = parameters.getValidator(); final ValidationResult validationResult = reportParameterValidator.validate( new ValidationResult(), parameters, parameterContext ); if ( validationResult.isEmpty() == false ) { throw new ReportParameterValidationException( "The parameters provided for this report are not valid.", validationResult ); } parameterValues = validationResult.getParameterValues(); } finally { parameterContext.close(); } } else { parameterValues = new ReportParameterValues(); } final PerformanceMonitorContext rawPerformanceMonitorContext = ClassicEngineBoot.getInstance().getObjectFactory().get( PerformanceMonitorContext.class ); this.performanceMonitorContext = new InternalPerformanceMonitorContext( rawPerformanceMonitorContext ); final InitialLayoutProcess layoutProcess = new InitialLayoutProcess( outputFunction, performanceMonitorContext ); this.reportInstancesShareConnection = "true".equals( processingContext.getConfiguration() .getConfigProperty( "org.pentaho.reporting.engine.classic.core.ReportInstancesShareConnections" ) ); this.processLevels = new HashSet<Integer>(); this.groupStarts = new FastStack<GroupStartRecord>(); this.errorHandler = IgnoreEverythingReportErrorHandler.INSTANCE; this.advanceHandler = BeginReportHandler.HANDLER; this.currentSubReport = -1; this.currentGroupIndex = ReportState.BEFORE_FIRST_GROUP; this.currentPresentationGroupIndex = ReportState.BEFORE_FIRST_GROUP; this.processStore = new ReportProcessStore(); this.functionStorage = new FunctionStorage(); this.structureFunctionStorage = new FunctionStorage(); this.sequenceCounter = 0; this.processKey = new ReportStateKey( null, ReportState.BEFORE_FIRST_ROW, 0, ReportState.BEFORE_FIRST_GROUP, -1, sequenceCounter, false, false ); this.dataFactoryManager = new DataFactoryManager(); this.subReportStorage = new SubReportStorage(); this.processHandle = new InternalProcessHandle( dataFactoryManager, performanceMonitorContext ); this.crosstabColumnSequenceCounter = new LongSequence( 10, -1 ); this.groupSequenceCounter = new LongSequence( 10, -1 ); this.groupSequenceCounter.set( 0, -1 ); final DefaultFlowController startFlowController = new DefaultFlowController( processingContext, report.getDataSchemaDefinition(), StateUtilities.computeParameterValueSet( report, parameterValues ), performanceMonitorContext ); final MasterReportProcessPreprocessor processPreprocessor = new MasterReportProcessPreprocessor( startFlowController ); final MasterReport processedReport = processPreprocessor.invokePreDataProcessing( report ); final DefaultFlowController flowController = processPreprocessor.getFlowController(); final Object dataCacheEnabledRaw = processedReport.getAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.DATA_CACHE ); final boolean dataCacheEnabled = designtime == false && Boolean.FALSE.equals( dataCacheEnabledRaw ) == false; final DataFactory sortingDataFactory = new SortingDataFactory( lookupDataFactory( processedReport ), performanceMonitorContext ); final CachingDataFactory dataFactory = new CachingDataFactory( sortingDataFactory, dataCacheEnabled ); dataFactory.initialize( new ProcessingDataFactoryContext( processingContext, dataFactory ) ); final FunctionStorageKey functionStorageKey = FunctionStorageKey.createKey( null, processedReport ); this.dataFactoryManager.store( functionStorageKey, dataFactory, true ); // eval query, query-limit and query-timeout this.flowController = flowController; final Integer queryLimitDefault = IntegerCache.getInteger( processedReport.getQueryLimit() ); final Integer queryTimeoutDefault = IntegerCache.getInteger( processedReport.getQueryTimeout() ); final String queryDefined = designtime ? "design-time-query" : processedReport.getQuery(); final Object queryRaw = evaluateExpression( processedReport.getAttributeExpression( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.QUERY ), queryDefined ); final Object queryLimitRaw = evaluateExpression( processedReport.getAttributeExpression( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.QUERY_LIMIT ), queryLimitDefault ); final Object queryTimeoutRaw = evaluateExpression( processedReport.getAttributeExpression( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.QUERY_TIMEOUT ), queryTimeoutDefault ); final List<SortConstraint> sortOrder = lookupSortOrder( processedReport ); this.query = (String) ConverterRegistry.convert( queryRaw, String.class, processedReport.getQuery() ); this.queryLimit = (Integer) ConverterRegistry.convert( queryLimitRaw, Integer.class, queryLimitDefault ); this.queryTimeout = (Integer) ConverterRegistry.convert( queryTimeoutRaw, Integer.class, queryTimeoutDefault ); DefaultFlowController postQueryFlowController = flowController.performQuery( dataFactory, query, queryLimit.intValue(), queryTimeout.intValue(), processingContext.getResourceBundleFactory(), sortOrder ); final MasterReportProcessPreprocessor postProcessor = new MasterReportProcessPreprocessor( postQueryFlowController ); final MasterReport fullReport = postProcessor.invokePreProcessing( processedReport ); postQueryFlowController = postProcessor.getFlowController(); if ( isStructureRunNeeded( processedReport ) == false ) { // Perform a static analysis on whether there is an External-element or Inline-Subreports or Crosstabs // if none, return unchanged this.structuralPreprocessingNeeded = false; } else { // otherwise process the report one time to walk through all eligible states. Record all subreports, // and then compute the runlevels based on what we have in the caches. this.structuralPreprocessingNeeded = true; this.processLevels.add( LayoutProcess.LEVEL_STRUCTURAL_PREPROCESSING ); postQueryFlowController.requireStructuralProcessing(); } final Expression[] expressions; if ( designtime ) { expressions = new Expression[ 0 ]; } else { expressions = fullReport.getExpressions().getExpressions(); } this.flowController = postQueryFlowController.activateExpressions( expressions, false ); this.report = new ReportDefinitionImpl( fullReport, fullReport.getPageDefinition() ); this.layoutProcess = new SubLayoutProcess( layoutProcess, computeStructureFunctions( fullReport.getStructureFunctions(), getFlowController().getReportContext().getOutputProcessorMetaData() ), fullReport.getObjectID() ); if ( StateUtilities.computeLevels( this.flowController, this.layoutProcess, processLevels ) ) { this.recorder = new DefaultGroupSizeRecorder(); } else { this.recorder = new EmptyGroupSizeRecorder(); } this.processKey = createKey(); return flowController.isQueryLimitReached(); } private List<SortConstraint> lookupSortOrder( final ReportDefinition report ) { Object attribute = report.getAttribute( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.COMPUTED_SORT_CONSTRAINTS ); if ( attribute instanceof List<?> ) { return (List<SortConstraint>) attribute; } return Collections.emptyList(); } private void initializeProcessingContext( final ProcessingContext processingContext, final MasterReport report ) { Configuration configuration = wrapForCompatibility( processingContext ); processingContext.getOutputProcessorMetaData().initialize( mapStaticMetaData( configuration, report ) ); } private Configuration mapStaticMetaData( final Configuration configuration, final MasterReport report ) { HierarchicalConfiguration hc = new HierarchicalConfiguration( configuration ); setConfigurationIfDefined( hc, "org.pentaho.reporting.engine.classic.core.layout.fontrenderer.ComplexTextLayout", report.getAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.COMPLEX_TEXT ) ); setConfigurationIfDefined( hc, "org.pentaho.reporting.engine.classic.core.WatermarkPrintedOnTopOfContent", report.getWatermark() .getAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.WATERMARK_PRINTED_ON_TOP ) ); return hc; } private void setConfigurationIfDefined( final ModifiableConfiguration config, final String configKey, final Object value ) { if ( value == null ) { return; } try { String valueText = ConverterRegistry.toAttributeValue( value ); config.setConfigProperty( configKey, valueText ); } catch ( final BeanException e ) { logger.info( String .format( "Ignoring invalid attribute-value override for configuration '%s' with value '%s'", configKey, value ) ); } } private Configuration wrapForCompatibility( final ProcessingContext processingContext ) { final int compatibilityLevel = processingContext.getCompatibilityLevel(); if ( compatibilityLevel < 0 ) { return processingContext.getConfiguration(); } if ( compatibilityLevel < ClassicEngineBoot.computeVersionId( 3, 999, 999 ) ) { // enable strict compatibility mode for reports older than 4.0. final HierarchicalConfiguration config = new HierarchicalConfiguration( processingContext.getConfiguration() ); config .setConfigProperty( "org.pentaho.reporting.engine.classic.core.legacy.WrapProgressMarkerInSection", "true" ); config.setConfigProperty( "org.pentaho.reporting.engine.classic.core.legacy.StrictCompatibility", "true" ); return config; } // this is a trunk or 4.0 or newer report. return processingContext.getConfiguration(); } private boolean isDesigntime() { return designtime; } private boolean isReportsShareConnections( final ReportDefinition report ) { final Object attribute = report.getAttribute( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.SHARED_CONNECTIONS ); if ( Boolean.TRUE.equals( attribute ) ) { return true; } if ( Boolean.FALSE.equals( attribute ) ) { return false; } return reportInstancesShareConnection; } public void initializeForSubreport( final InlineSubreportMarker[] subReports, final int subReportIndex, final ProcessState parentState ) throws ReportProcessingException { if ( parentState == null ) { throw new NullPointerException(); } this.designtime = parentState.designtime; this.crosstabColumnSequenceCounter = new LongSequence( 10, -1 ); this.groupSequenceCounter = new LongSequence( 10, -1 ); this.groupSequenceCounter.set( 0, -1 ); this.recorder = (GroupSizeRecorder) parentState.recorder.clone(); this.recorder.reset(); this.performanceMonitorContext = parentState.performanceMonitorContext; this.reportInstancesShareConnection = parentState.reportInstancesShareConnection; this.groupStarts = new FastStack<GroupStartRecord>(); this.parentSubReportState = parentState; this.advanceHandler = BeginReportHandler.HANDLER; this.errorHandler = parentState.errorHandler; this.functionStorage = parentState.functionStorage; this.structureFunctionStorage = parentState.structureFunctionStorage; this.processStore = parentState.processStore; this.currentGroupIndex = ReportState.BEFORE_FIRST_GROUP; this.currentPresentationGroupIndex = ReportState.BEFORE_FIRST_GROUP; this.currentSubReport = subReportIndex; this.subReports = subReports.clone(); this.dataFactoryManager = parentState.dataFactoryManager; this.subReportStorage = parentState.subReportStorage; this.structuralPreprocessingNeeded = parentState.structuralPreprocessingNeeded; this.processLevels = parentState.processLevels; this.sequenceCounter = parentState.getSequenceCounter() + 1; this.currentSubReportMarker = subReports[ subReportIndex ]; this.inlineProcess = parentState.isInlineProcess() || currentSubReportMarker.getProcessType() == SubReportProcessType.INLINE; final SubReport subreportFromMarker = currentSubReportMarker.getSubreport(); final FunctionStorageKey functionStorageKey = FunctionStorageKey.createKey( parentSubReportState.getProcessKey(), subreportFromMarker ); final boolean needPreProcessing; final SubReport initialSubReport; if ( subReportStorage.contains( functionStorageKey ) ) { initialSubReport = subReportStorage.restore( functionStorageKey ); initialSubReport.reconnectParent( subreportFromMarker.getParentSection() ); applyCurrentStyleAndAttributes( subreportFromMarker, initialSubReport ); needPreProcessing = false; } else { initialSubReport = subreportFromMarker.derive( true ); initialSubReport.reconnectParent( subreportFromMarker.getParentSection() ); needPreProcessing = true; } final DefaultFlowController parentStateFlowController = parentState.getFlowController(); final ResourceBundleFactory resourceBundleFactory = parentState.getResourceBundleFactory(); if ( isSubReportInvisible( initialSubReport, parentStateFlowController ) ) { // make it a minimum effort report, but still enter the loop. final ReportDefinition parentReport = parentState.getReport(); final SubReport dummyReport = new SubReport( functionStorageKey.getReportId() ); this.report = new ReportDefinitionImpl( dummyReport, parentReport.getPageDefinition(), subreportFromMarker.getParentSection() ); this.flowController = parentStateFlowController.derive(); this.advanceHandler = EndSubReportHandler.HANDLER; this.layoutProcess = new SubLayoutProcess( parentState.layoutProcess, computeStructureFunctions( initialSubReport.getStructureFunctions(), flowController.getReportContext().getOutputProcessorMetaData() ), this.report.getObjectID() ); } else { DataFactory dataFactory = dataFactoryManager.restore( functionStorageKey, isReportsShareConnections( initialSubReport ) ); final DefaultFlowController postPreProcessingFlowController; final SubReport preDataSubReport; if ( dataFactory == null ) { final SubReportProcessPreprocessor preprocessor = new SubReportProcessPreprocessor( parentStateFlowController ); preDataSubReport = preprocessor.invokePreDataProcessing( initialSubReport ); postPreProcessingFlowController = preprocessor.getFlowController(); final DataFactory subreportDf = lookupDataFactory( preDataSubReport ); final boolean dataCacheEnabled = isCacheEnabled( preDataSubReport ); if ( subreportDf == null ) { // subreport does not define a own factory, we reuse the parent's data-factory in the master-row. dataFactory = new EmptyDataFactory(); } else { // subreport comes with an own factory, so open the gates .. final DataFactory sortingDataFactory = new SortingDataFactory( subreportDf, performanceMonitorContext ); final CachingDataFactory cdataFactory = new CachingDataFactory( sortingDataFactory, dataCacheEnabled ); final ProcessingContext context = postPreProcessingFlowController.getReportContext(); cdataFactory.initialize( new ProcessingDataFactoryContext( context, cdataFactory ) ); dataFactoryManager.store( functionStorageKey, cdataFactory, isReportsShareConnections( preDataSubReport ) ); dataFactory = cdataFactory; } } else { preDataSubReport = initialSubReport; postPreProcessingFlowController = parentStateFlowController; } // And now initialize the sub-report. final ParameterMapping[] inputMappings = preDataSubReport.getInputMappings(); final ParameterMapping[] exportMappings = preDataSubReport.getExportMappings(); // eval query, query-limit and query-timeout this.flowController = postPreProcessingFlowController.performInitSubreport( dataFactory, inputMappings, resourceBundleFactory ); final Integer queryLimitDefault = IntegerCache.getInteger( preDataSubReport.getQueryLimit() ); final Integer queryTimeoutDefault = IntegerCache.getInteger( preDataSubReport.getQueryTimeout() ); final Object queryRaw = evaluateExpression( preDataSubReport.getAttributeExpression( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.QUERY ), preDataSubReport.getQuery() ); final Object queryLimitRaw = evaluateExpression( preDataSubReport.getAttributeExpression( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.QUERY_LIMIT ), queryLimitDefault ); final Object queryTimeoutRaw = evaluateExpression( preDataSubReport.getAttributeExpression( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.QUERY_TIMEOUT ), queryTimeoutDefault ); final String queryDefined = designtime ? "design-time-query" : preDataSubReport.getQuery(); this.query = (String) ConverterRegistry.convert( queryRaw, String.class, queryDefined ); this.queryLimit = (Integer) ConverterRegistry.convert( queryLimitRaw, Integer.class, queryLimitDefault ); this.queryTimeout = (Integer) ConverterRegistry.convert( queryTimeoutRaw, Integer.class, queryTimeoutDefault ); final List<SortConstraint> sortOrder = lookupSortOrder( preDataSubReport ); DefaultFlowController postQueryFlowController = flowController .performSubReportQuery( query, queryLimit.intValue(), queryTimeout.intValue(), exportMappings, sortOrder ); final ProxyDataSchemaDefinition schemaDefinition = new ProxyDataSchemaDefinition( preDataSubReport.getDataSchemaDefinition(), postQueryFlowController.getMasterRow().getDataSchemaDefinition() ); postQueryFlowController = postQueryFlowController.updateDataSchema( schemaDefinition ); SubReport fullReport = preDataSubReport; DefaultFlowController fullFlowController = postQueryFlowController; if ( needPreProcessing ) { final SubReportProcessPreprocessor preprocessor = new SubReportProcessPreprocessor( postQueryFlowController ); fullReport = preprocessor.invokePreProcessing( preDataSubReport ); fullFlowController = preprocessor.getFlowController(); subReportStorage.store( functionStorageKey, fullReport ); } this.report = new ReportDefinitionImpl( fullReport, fullReport.getPageDefinition(), subreportFromMarker.getParentSection() ); final Expression[] structureFunctions = getStructureFunctionStorage().restore( functionStorageKey ); if ( structureFunctions != null ) { final StructureFunction[] functions = new StructureFunction[ structureFunctions.length ]; //noinspection SuspiciousSystemArraycopy System.arraycopy( structureFunctions, 0, functions, 0, structureFunctions.length ); this.layoutProcess = new SubLayoutProcess( parentState.layoutProcess, functions, this.report.getObjectID() ); } else { final StructureFunction[] functions = computeStructureFunctions( fullReport.getStructureFunctions(), fullFlowController.getReportContext().getOutputProcessorMetaData() ); this.layoutProcess = new SubLayoutProcess( parentState.layoutProcess, functions, this.report.getObjectID() ); } boolean preserve = true; Expression[] expressions = getFunctionStorage().restore( functionStorageKey ); if ( expressions == null ) { // ok, it seems we have entered a new subreport .. // we use the expressions from the report itself .. if ( designtime ) { expressions = new Expression[ 0 ]; } else { expressions = fullReport.getExpressions().getExpressions(); } preserve = false; } this.flowController = fullFlowController.activateExpressions( expressions, preserve ); this.flowController = this.flowController.refreshDataRow(); // now a bunch of paranoid assertions, just in case I missed something. if ( this.report.getParentSection() == null ) { throw new InvalidReportStateException(); } if ( this.report.getParentSection().getReportDefinition() != this.parentSubReportState.getReport() ) { throw new InvalidReportStateException(); } final int processingLevel = flowController.getReportContext().getProcessingLevel(); if ( processingLevel == LayoutProcess.LEVEL_PAGINATE ) { if ( this.parentSubReportState.isInItemGroup() ) { if ( this.parentSubReportState.getReport().getDetailsFooter().getComputedStyle() == null ) { throw new InvalidReportStateException(); } } } } StateUtilities.computeLevels( this.flowController, this.layoutProcess, processLevels ); this.processKey = createKey(); } public ReportProcessStore getProcessStore() { return processStore; } private DataFactory lookupDataFactory( final AbstractReportDefinition report ) { if ( designtime ) { return new DesignTimeDataFactory(); } return report.getDataFactory(); } private void applyCurrentStyleAndAttributes( final SubReport subreportFromMarker, final SubReport report ) { // derive would regenerate instance-IDs, which is not advisable. report.getStyle().copyFrom( subreportFromMarker.getStyle() ); report.copyAttributes( subreportFromMarker.getAttributes() ); } private boolean isSubReportInvisible( final SubReport report, final DefaultFlowController flowController ) { final int processingLevel = flowController.getReportContext().getProcessingLevel(); if ( processingLevel != LayoutProcess.LEVEL_PAGINATE ) { // outside return false; } if ( designtime ) { return false; } OutputProcessorMetaData metaData = flowController.getReportContext().getOutputProcessorMetaData(); if ( metaData.isFeatureSupported( OutputProcessorFeature.DESIGNTIME ) ) { final Object attribute = report.getAttribute( AttributeNames.Designtime.NAMESPACE, AttributeNames.Designtime.HIDE_IN_LAYOUT_GUI_ATTRIBUTE ); if ( Boolean.TRUE.equals( attribute ) ) { return true; } return false; } else { final StyleSheet computedStyle = report.getComputedStyle(); if ( computedStyle.getBooleanStyleProperty( ElementStyleKeys.VISIBLE ) ) { return false; } return true; } } private static boolean isCacheEnabled( ReportDefinition reportDefinition ) { while ( reportDefinition != null ) { final Object dataCacheEnabledRaw = reportDefinition.getAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.DATA_CACHE ); if ( Boolean.FALSE.equals( dataCacheEnabledRaw ) ) { return false; } final Section parentSection = reportDefinition.getParentSection(); if ( parentSection == null ) { break; } reportDefinition = parentSection.getReportDefinition(); } return true; } private Object evaluateExpression( final Expression expression, final Object defaultValue ) { if ( expression == null ) { return defaultValue; } if ( designtime ) { return defaultValue; } final Expression evalExpression = expression.getInstance(); final InlineDataRowRuntime runtime = new InlineDataRowRuntime(); runtime.setState( this ); final ExpressionRuntime oldRuntime = evalExpression.getRuntime(); try { evalExpression.setRuntime( runtime ); return evalExpression.getValue(); } catch ( final Exception e ) { logger.debug( "Failed to evaluate expression " + expression, e ); return defaultValue; } finally { evalExpression.setRuntime( oldRuntime ); } } public int[] getRequiredRuntimeLevels() { processLevels.add( IntegerCache.getInteger( LayoutProcess.LEVEL_PAGINATE ) ); final int[] retval = new int[ processLevels.size() ]; final Integer[] levels = processLevels.toArray( new Integer[ processLevels.size() ] ); Arrays.sort( levels, new StateUtilities.DescendingComparator<Integer>() ); for ( int i = 0; i < levels.length; i++ ) { final Integer level = levels[ i ]; retval[ i ] = level.intValue(); } return retval; } private StructureFunction[] computeStructureFunctions( final StructureFunction[] fromReport, final OutputProcessorMetaData metaData ) { final ArrayList<StructureFunction> e = new ArrayList<StructureFunction>( Arrays.asList( fromReport ) ); if ( structuralPreprocessingNeeded ) { e.add( new CrosstabProcessorFunction() ); } if ( isDesigntime() == false ) { e.add( new AttributeExpressionsEvaluator() ); e.add( new SheetNameFunction() ); e.add( new StyleExpressionsEvaluator() ); e.add( new CellFormatFunction() ); e.add( new WizardItemHideFunction() ); } e.add( new MetaDataStyleEvaluator() ); e.add( new StyleResolvingEvaluator() ); Collections.sort( e, new StructureFunctionComparator() ); return e.toArray( new StructureFunction[ e.size() ] ); } public boolean isSubReportExecutable() { final Expression expression = getReport().getAttributeExpression( AttributeNames.Core.NAMESPACE, AttributeNames.Core.SUBREPORT_ACTIVE ); if ( expression != null ) { // the master-report state will only be non-null for subreports. final InlineDataRowRuntime dataRowRuntime = new InlineDataRowRuntime(); dataRowRuntime.setState( this ); expression.setRuntime( dataRowRuntime ); try { final Object value = expression.getValue(); // the expression has to explicitly return false as a value to disable the report processing of // subreports. Just returning null or a non-boolean value is not enough. This is a safety measure // so that if in doubt we print more data than to little. if ( Boolean.FALSE.equals( value ) ) { return false; } if ( "false".equals( String.valueOf( value ) ) ) { return false; } return true; } finally { expression.setRuntime( null ); } } return true; } public ProcessState returnFromSubReport( final LayoutProcess layoutProcess ) throws ReportProcessingException { final ProcessState state = deriveForAdvance(); state.layoutProcess = (LayoutProcess) layoutProcess.clone(); return state; } public ProcessState restart() throws ReportProcessingException { if ( getParentState() != null ) { throw new IllegalStateException( "Cannot reset a state that is a subreport state" ); } final ProcessState state = this.deriveForStorage(); state.crosstabColumnSequenceCounter.clear(); state.groupSequenceCounter.clear(); state.groupSequenceCounter.set( 0, -1 ); state.recorder.reset(); state.currentSubReport = -1; state.currentGroupIndex = ReportState.BEFORE_FIRST_GROUP; state.currentPresentationGroupIndex = ReportState.BEFORE_FIRST_GROUP; if ( state.groupStarts.isEmpty() == false ) { throw new IllegalStateException(); } state.setAdvanceHandler( BeginReportHandler.HANDLER ); final ReportStateKey parentStateKey; final ReportState parentState = this.getParentSubReportState(); if ( parentState == null ) { parentStateKey = null; } else { parentStateKey = parentState.getProcessKey(); } final CachingDataFactory dataFactory = state.dataFactoryManager .restore( FunctionStorageKey.createKey( parentStateKey, state.getReport() ), isReportsShareConnections( report ) ); if ( dataFactory == null ) { throw new ReportProcessingException( "No data factory on restart()? Somewhere we went wrong." ); } final DefaultFlowController fc = state.getFlowController(); final DefaultFlowController cfc = fc.restart(); final DefaultFlowController qfc = cfc.performQuery( dataFactory, query, queryLimit.intValue(), queryTimeout.intValue(), fc.getMasterRow().getResourceBundleFactory(), lookupSortOrder( state.report ) ); final Expression[] expressions = getFunctionStorage().restore( FunctionStorageKey.createKey( null, state.getReport() ) ); final DefaultFlowController efc = qfc.activateExpressions( expressions, true ); state.setFlowController( efc ); state.sequenceCounter += 1; state.processKey = state.createKey(); return state; } public ReportProcessingErrorHandler getErrorHandler() { return errorHandler; } public void setErrorHandler( final ReportProcessingErrorHandler errorHandler ) { this.errorHandler = errorHandler; } public void setSequenceCounter( final int sequenceCounter ) { this.sequenceCounter = sequenceCounter; this.processKey = this.createKey(); } public int getSequenceCounter() { return sequenceCounter; } public InlineSubreportMarker getCurrentSubReportMarker() { return currentSubReportMarker; } public boolean isInlineProcess() { return inlineProcess; } public SubReportProcessType getSubreportProcessingType() { InlineSubreportMarker cm = getCurrentSubReportMarker(); if ( cm == null ) { return SubReportProcessType.BANDED; } return cm.getProcessType(); } /** * This is a more expensive version of the ordinary derive. This method creates a separate copy of the layout-process * so that this operation is expensive in memory and CPU usage. * * @return the derived state. */ public ProcessState deriveForPagebreak() { try { final ProcessState processState = clone(); processState.flowController = flowController.derive(); processState.report = report.clone(); processState.layoutProcess = layoutProcess.deriveForPagebreak(); return processState; } catch ( final CloneNotSupportedException e ) { throw new IllegalStateException( "Clone failed but I dont know why .." ); } } public ProcessState deriveForAdvance() { try { final ProcessState processState = clone(); processState.sequenceCounter += 1; processState.processKey = processState.createKey(); return processState; } catch ( final CloneNotSupportedException e ) { throw new IllegalStateException( "Clone failed but I dont know why .." ); } } public ProcessState deriveForStorage() { try { final ProcessState result = clone(); result.flowController = flowController.derive(); result.report = report.clone(); result.layoutProcess = layoutProcess.deriveForStorage(); return result; } catch ( final CloneNotSupportedException e ) { throw new IllegalStateException( "Clone failed but I dont know why .." ); } } public ProcessState clone() throws CloneNotSupportedException { final ProcessState result = (ProcessState) super.clone(); result.groupSequenceCounter = (LongSequence) groupSequenceCounter.clone(); result.crosstabColumnSequenceCounter = (LongSequence) crosstabColumnSequenceCounter.clone(); result.groupStarts = groupStarts.clone(); result.processKey = result.createKey(); result.recorder = (GroupSizeRecorder) recorder.clone(); return result; } public AdvanceHandler getAdvanceHandler() { return advanceHandler; } private ReportStateKey createKey() { final ProcessState parent = (ProcessState) getParentState(); if ( parent != null ) { return new ReportStateKey( parent.createKey(), getCurrentRow(), getEventCode(), getCurrentGroupIndex(), getCurrentSubReport(), sequenceCounter, advanceHandler.isRestoreHandler(), isInlineProcess() ); } return new ReportStateKey( null, getCurrentRow(), getEventCode(), getCurrentGroupIndex(), getCurrentSubReport(), sequenceCounter, advanceHandler.isRestoreHandler(), false ); } public void setAdvanceHandler( final AdvanceHandler advanceHandler ) { if ( advanceHandler == null ) { throw new NullPointerException(); } this.advanceHandler = advanceHandler; this.processKey = null; } public final ProcessState advance() throws ReportProcessingException { return advanceHandler.advance( this ); } public final ProcessState commit() throws ReportProcessingException { final ProcessState commit = advanceHandler.commit( this ); commit.processKey = commit.createKey(); return commit; } public int getCurrentRow() { return this.flowController.getMasterRow().getCursor(); } public int getCurrentDataItem() { return this.flowController.getMasterRow().getRawDataCursor(); } public int getProgressLevel() { return flowController.getReportContext().getProgressLevel(); } public int getProgressLevelCount() { return flowController.getReportContext().getProgressLevelCount(); } public boolean isPrepareRun() { return flowController.getReportContext().isPrepareRun(); } public int getLevel() { return flowController.getReportContext().getProcessingLevel(); } public boolean isFinish() { return advanceHandler.isFinish(); } public int getEventCode() { return advanceHandler.getEventCode(); } public int getCurrentGroupIndex() { return currentGroupIndex; } public void enterGroup() { recorder.enterGroup(); currentGroupIndex += 1; final Group group = report.getGroup( currentGroupIndex ); groupStarts.push( new GroupStartRecord( getCurrentRow(), group.getName(), group.getGeneratedName() ) ); groupSequenceCounter.increment( currentGroupIndex ); groupSequenceCounter.set( currentGroupIndex + 1, 0 ); if ( groupStarts.size() != currentGroupIndex + 1 ) { throw new IllegalStateException(); } } public void leaveGroup() { recorder.leaveGroup(); if ( groupStarts.size() != currentGroupIndex + 1 ) { throw new IllegalStateException(); } currentGroupIndex -= 1; groupStarts.pop(); } public int getPresentationGroupIndex() { return currentPresentationGroupIndex; } public void enterPresentationGroup() { currentPresentationGroupIndex += 1; } public void leavePresentationGroup() { currentPresentationGroupIndex -= 1; } public ReportDefinition getReport() { return report; } public int getCurrentSubReport() { return currentSubReport; } public ReportState getParentState() { if ( suspendedState != null ) { return suspendedState; } if ( parentSubReportState != null ) { return parentSubReportState; } return null; } public ReportState getParentSubReportState() { return parentSubReportState; } public FunctionStorage getStructureFunctionStorage() { return structureFunctionStorage; } public FunctionStorage getFunctionStorage() { return functionStorage; } public DefaultFlowController getFlowController() { return flowController; } public void setFlowController( final DefaultFlowController flowController ) { if ( flowController == null ) { throw new NullPointerException(); } this.flowController = flowController; this.processKey = null; } public LayoutProcess getLayoutProcess() { return layoutProcess; } public ReportStateKey getProcessKey() { if ( processKey == null ) { processKey = createKey(); } return processKey; } public DataRow getDataRow() { return flowController.getMasterRow().getGlobalView(); } public int getNumberOfRows() { final MasterDataRow masterRow = flowController.getMasterRow(); return masterRow.getRowCount(); } /** * Fires a 'page-started' event. * * @param baseEvent the type of the base event which caused the page start to be triggered. */ public void firePageStartedEvent( final int baseEvent ) { final ReportEvent event = new ReportEvent( this, ReportEvent.PAGE_STARTED | baseEvent ); flowController = flowController.fireReportEvent( event ); layoutProcess.fireReportEvent( event ); } /** * Fires a '<code>page-finished</code>' event. The <code>pageFinished(...)</code> method is called for every report * function. */ public void firePageFinishedEvent( final boolean noParentPassing ) { final int eventCode = ReportEvent.PAGE_FINISHED | ( noParentPassing ? ReportEvent.NO_PARENT_PASSING_EVENT : 0 ); final ReportEvent event = new ReportEvent( this, eventCode ); flowController = flowController.fireReportEvent( event ); layoutProcess.fireReportEvent( event ); } protected void fireReportEvent() { final int eventCode = advanceHandler.getEventCode(); if ( ( eventCode & ProcessState.ARTIFICIAL_EVENT_CODE ) == ProcessState.ARTIFICIAL_EVENT_CODE ) { throw new IllegalStateException( "Cannot fire artificial events." ); } final ReportEvent event = new ReportEvent( this, eventCode ); flowController = flowController.fireReportEvent( event ); layoutProcess.fireReportEvent( event ); } /** * Returns true if this is the last item in the group, and false otherwise. This checks the group condition and all * conditions of all subgroups. * * @param rootGroup the root group that should be checked. * @param currentDataRow the current data row. * @param nextDataRow the next data row, or null, if this is the last datarow. * @return A flag indicating whether or not the current item is the last in its group. */ public static boolean isLastItemInGroup( final Group rootGroup, final MasterDataRow currentDataRow, final MasterDataRow nextDataRow ) { // return true if this is the last row in the model. if ( currentDataRow.isAdvanceable() == false || nextDataRow == null ) { return true; } final DataRow nextView = nextDataRow.getGlobalView(); Group g = rootGroup; while ( g != null ) { if ( g.isGroupChange( nextView ) ) { return true; } // groups are never directly nested into each other. They always have a group-body between each group instance. final Section parentSection = g.getParentSection(); if ( parentSection == null ) { return false; } final Section maybeGroup = parentSection.getParentSection(); if ( maybeGroup instanceof Group ) { g = (Group) maybeGroup; } else { g = null; } } return false; } public boolean isSubReportEvent() { return getParentSubReportState() != null; } public InlineSubreportMarker[] getSubReports() { return subReports.clone(); } public ProcessStateHandle getProcessHandle() { return processHandle; } public void setInItemGroup( final boolean inItemGroup ) { if ( inItemGroup ) { recorder.enterItems(); } else { recorder.leaveItems(); } this.inItemGroup = inItemGroup; } public boolean isInItemGroup() { return inItemGroup; } public ResourceBundleFactory getResourceBundleFactory() { return flowController.getMasterRow().getResourceBundleFactory(); } public boolean isArtifcialState() { return ( advanceHandler.getEventCode() & ReportEvent.ARTIFICIAL_EVENT_CODE ) != 0; } public GroupingState createGroupingState() { return new DefaultGroupingState( currentGroupIndex, groupStarts.clone() ); } private boolean isStructureRunNeeded( final Section section ) { final int count = section.getElementCount(); for ( int i = 0; i < count; i++ ) { final ReportElement element = section.getElement( i ); final Object type = element.getAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.ELEMENT_TYPE ); if ( type instanceof ExternalElementType ) { return true; } if ( element instanceof CrosstabGroup ) { return true; } else if ( element instanceof SubReport ) { return true; } else if ( element instanceof RootLevelBand ) { final RootLevelBand band = (RootLevelBand) element; if ( band.getSubReportCount() > 0 ) { return true; } if ( isStructureRunNeeded( (Section) element ) ) { return true; } } else if ( element instanceof Section ) { if ( isStructureRunNeeded( (Section) element ) ) { return true; } } } return false; } public boolean isStructuralPreprocessingNeeded() { return structuralPreprocessingNeeded; } public void advanceCursor() { recorder.advanceItems(); } public Integer getPredictedStateCount() { return recorder.getPredictedStateCount(); } public boolean isCrosstabActive() { return flowController.isCrosstabActive(); } public String toString() { final StringBuilder b = new StringBuilder(); b.append( "ProcessState={" ); b.append( "runLevel=" ).append( getLevel() ); b.append( ", key=" ).append( getProcessKey() ); b.append( "}" ); return b.toString(); } public ProcessState recordCrosstabRowState() { // record the flow controller and all expressions for a later temporary rollback. final ProcessState next = deriveForAdvance(); next.flowController = flowController.recordCrosstabRowState(); next.crosstabColumnSequenceCounter.clear(); next.crosstabColumnSequenceCounter.fill( -1 ); return next; } /** * Reset the state to use the stored flow-controller for the summary calculation. * * @return */ public ProcessState replayStoredCrosstabRowState() { final ProcessState next = deriveForAdvance(); next.replayStoredCrosstabGroup = currentGroupIndex + 1; next.suspendedState = this; next.flowController = flowController.replayStoredCrosstabRowState(); next.processKey = next.createKey(); // DebugLog.log("ProcessState:replay " + processKey); return next; } public int getReplayStoredCrosstabGroup() { return replayStoredCrosstabGroup; } public AdvanceHandler getPostSummaryRowAdvanceHandler() { return postSummaryRowAdvanceHandler; } public void setPostSummaryRowAdvanceHandler( final AdvanceHandler postSummaryRowAdvanceHandler ) { this.postSummaryRowAdvanceHandler = postSummaryRowAdvanceHandler; } public ProcessState finishReplayingStoredCrosstabRowState() throws ReportProcessingException { final ProcessState next = this.suspendedState.deriveForAdvance(); next.layoutProcess = (LayoutProcess) this.layoutProcess.clone(); next.flowController = flowController.derive(); next.advanceHandler = this.advanceHandler; next.flowController.getMasterRow().validateReplayFinished(); // DebugLog.log("ProcessState:finish-replay " + processKey); return next; } public void clearStoredCrosstabRowState() { this.flowController = flowController.clearRecordedCrosstabRowState(); } public long getGroupSequenceCounter( final int groupIndex ) { return groupSequenceCounter.get( groupIndex ); } public long getCrosstabColumnSequenceCounter( final int groupIndex ) { return crosstabColumnSequenceCounter.get( groupIndex ); } public void crosstabResetColumnIndices() { crosstabColumnSequenceCounter.clear(); crosstabColumnSequenceCounter.fill( -1 ); } public void crosstabIncrementColumnCounter() { crosstabColumnSequenceCounter.increment( currentGroupIndex ); } public PerformanceMonitorContext getPerformanceMonitorContext() { return performanceMonitorContext; } }