/* * 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 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.layout.output; import java.util.ArrayList; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.engine.classic.core.ReportProcessingException; import org.pentaho.reporting.engine.classic.core.util.WeakReferenceList; import org.pentaho.reporting.libraries.base.config.ExtendedConfigurationWrapper; /** * The ReportState list stores a report states for the beginning of every page. The list is filled on repagination and * read when a report or a page of the report is printed. * <p/> * Important: This list stores page start report states, not arbitary report states. These ReportStates are special: * they can be reproduced by calling processPage on the report. * <p/> * Internally this list is organized as a list of WeakReferenceLists, where every WeakReferenceList stores a certain * number of page states. The first 20 states are stored in an ordinary list with strong-references, so these states * never get GarbageCollected (and so they must never be restored by reprocessing them). The next 100 states are stored * in 4-element ReferenceLists, so if a reference is lost, only 4 states have to be reprocessed. All other states are * stored in 10-element lists. * * @author Thomas Morgner */ public class DefaultPageStateList implements PageStateList { private static final Log logger = LogFactory.getLog( DefaultPageStateList.class ); /** * The position of the master element in the list. A greater value will reduce the not-freeable memory used by the * list, but restoring a single page will require more time. */ /** * The maxmimum masterposition size. */ private static final int MASTERPOSITIONS_MAX = 10; /** * The medium masterposition size. */ private static final int MASTERPOSITIONS_MED = 4; /** * The max index that will be stored in the primary list. */ private static final int PRIMARY_MAX = 20; /** * The max index that will be stored in the master4 list. */ private static final int MASTER4_MAX = 100; /** * Internal WeakReferenceList that is capable to restore its elements. The elements in this list are page start report * states. */ private final class MasterList extends WeakReferenceList<InternalStorageState> { /** * The master list. */ private final DefaultPageStateList master; /** * Creates a new master list. * * @param list * the list. * @param maxChildCount * the maximum number of elements in this list. */ private MasterList( final DefaultPageStateList list, final int maxChildCount ) { super( maxChildCount ); this.master = list; } public Object clone() throws CloneNotSupportedException { return super.clone(); } /** * Function to restore the state of a child after the child was garbage collected. * * @param index * the index. * @return the restored ReportState of the given index, or null, if the state could not be restored. */ protected InternalStorageState restoreChild( final int index ) { InternalStorageState master = (InternalStorageState) getMaster(); if ( master == null ) { throw new IllegalStateException( "We cannot have a master list without a master state" ); } try { final int max = getChildPos( index ); logger.info( "Restoring weak state " + master.getStorePosition() + " for # " + max ); if ( master.isValidRestorePoint() == false ) { // not a safe point, so restore the master first .. final InternalStorageState state = DefaultPageStateList.this.restoreState( master.getStorePosition(), master.getLastSavePosition(), getInternal( master.getLastSavePosition() ) ); setMaster( state ); master = state; } return restoreState( max, master ); } catch ( Exception rpe ) { DefaultPageStateList.logger.debug( "Caught exception while restoring a saved page-state", rpe ); throw new IllegalStateException( "Something went wrong while trying to restore the child #" + index ); } } /** * Internal handler function restore a state. Count denotes the number of pages required to be processed to restore * the page, when the reportstate master is used as source element. * * @param count * the count. * @param rootstate * the root state. * @return the report state. * @throws org.pentaho.reporting.engine.classic.core.ReportProcessingException * if there was a problem processing the report. */ private InternalStorageState restoreState( final int count, final InternalStorageState rootstate ) throws ReportProcessingException { if ( rootstate == null ) { throw new NullPointerException( "Master is null" ); } InternalStorageState state = rootstate; for ( int i = 0; i <= count; i++ ) { final ReportProcessor pageProcess = master.getPageProcess(); final PageState pageState = pageProcess.processPage( state.getPageState(), false ); if ( pageState == null ) { throw new IllegalStateException( "State returned is null: Report processing reached premature end-point." ); } state = new InternalStorageState( pageState, rootstate.getStorePosition() + i, rootstate.getLastSavePosition() ); if ( state.isValidRestorePoint() ) { set( state, i + 1 ); } } return state; } } private static class InternalStorageState { private PageState pageState; private int storePosition; private int lastSavePosition; private InternalStorageState( final PageState pageState, final int storePosition, final int lastSavePosition ) { this.pageState = pageState; this.storePosition = storePosition; this.lastSavePosition = lastSavePosition; } public PageState getPageState() { return pageState; } public int getStorePosition() { return storePosition; } public int getLastSavePosition() { return lastSavePosition; } public boolean isValidRestorePoint() { if ( pageState == null ) { return false; } return pageState.isSafeToStoreEarly(); } } /** * The list of master states. This is a list of WeakReferenceLists. These WeakReferenceLists contain their master * state as first child. The weakReferenceLists have a maxSize of 10, so every 10th state will protected from being * garbageCollected. */ private ArrayList<MasterList> masterStates10; // all states > 120 /** * The list of master states. This is a list of WeakReferenceLists. These WeakReferenceLists contain their master * state as first child. The weakReferenceLists have a maxSize of 4, so every 4th state will protected from being * garbageCollected. */ private ArrayList<MasterList> masterStates4; // all states from 20 - 120 /** * The list of primary states. This is a list of ReportStates and is used to store the first 20 elements of this state * list. */ private ArrayList<InternalStorageState> primaryStates; // all states from 0 - 20 /** * The number of elements in this list. */ private int size; private ReportProcessor pageProcess; private int primaryPoolSize; private int secondaryPoolSize; private int secondaryPoolFrequency; private int tertiaryPoolFrequency; private int lastSafePosition; /** * Creates a new reportstatelist. The list will be filled using the specified report and output target. Filling of the * list is done elsewhere. * * @param proc * the reportprocessor used to restore lost states (null not permitted). * @throws NullPointerException * if the report processor is <code>null</code>. */ public DefaultPageStateList( final ReportProcessor proc ) { if ( proc == null ) { throw new NullPointerException( "ReportProcessor null" ); } this.pageProcess = proc; final ExtendedConfigurationWrapper config = new ExtendedConfigurationWrapper( proc.getConfiguration() ); this.primaryPoolSize = config.getIntProperty( "org.pentaho.reporting.engine.classic.core.performance.pagestates.PrimaryPoolSize", PRIMARY_MAX ); this.secondaryPoolFrequency = config.getIntProperty( "org.pentaho.reporting.engine.classic.core.performance.pagestates.SecondaryPoolFrequency", MASTERPOSITIONS_MED ); this.secondaryPoolSize = config.getIntProperty( "org.pentaho.reporting.engine.classic.core.performance.pagestates.SecondaryPoolSize", MASTER4_MAX ) + primaryPoolSize; this.tertiaryPoolFrequency = config.getIntProperty( "org.pentaho.reporting.engine.classic.core.performance.pagestates.TertiaryPoolFrequency", MASTERPOSITIONS_MAX ); if ( primaryPoolSize < 1 ) { throw new IllegalStateException( "Invalid configuration: Primary pool must be >= 1" ); } if ( secondaryPoolSize < primaryPoolSize ) { throw new IllegalStateException( "Invalid configuration: Secondary pool must be >= primary pool" ); } if ( secondaryPoolFrequency < 1 ) { throw new IllegalStateException( "Invalid configuration: Secondary pool frequency must be >= 1" ); } if ( tertiaryPoolFrequency < 1 ) { throw new IllegalStateException( "Invalid configuration: Tertiary pool frequency must be >= 1" ); } primaryStates = new ArrayList<InternalStorageState>( primaryPoolSize ); masterStates4 = new ArrayList<MasterList>( secondaryPoolSize ); masterStates10 = new ArrayList<MasterList>(); } /** * Returns the index of the WeakReferenceList in the master list. * * @param pos * the position. * @param maxListSize * the maximum list size. * @return the position within the masterStateList. */ private int getMasterPos( final int pos, final int maxListSize ) { // return (int) Math.floor(pos / maxListSize); return ( pos / maxListSize ); } protected ReportProcessor getPageProcess() { return pageProcess; } /** * Returns the number of elements in this list. * * @return the number of elements in the list. */ public int size() { return this.size; } /** * Adds this report state to the end of the list. * * @param pageState * the report state. */ public void add( final PageState pageState ) { if ( pageState == null ) { throw new NullPointerException(); } final int size = size(); final InternalStorageState state; if ( pageState.isSafeToStoreEarly() == false ) { state = new InternalStorageState( null, size, lastSafePosition ); } else { lastSafePosition = size; pageState.prepareStorage(); state = new InternalStorageState( pageState, size, lastSafePosition ); } // the first 20 Elements are stored directly into an ArrayList if ( size < primaryPoolSize ) { primaryStates.add( state ); this.size++; } else if ( size < secondaryPoolSize ) { // the next 100 Elements are stored into a list of 4-element weakReference // list. So if an Element gets lost (GCd), only 4 states need to be replayed. final int secPos = size - primaryPoolSize; final int masterPos = getMasterPos( secPos, secondaryPoolFrequency ); if ( masterPos >= masterStates4.size() ) { final MasterList master = new MasterList( this, secondaryPoolFrequency ); masterStates4.add( master ); master.add( state ); } else { final MasterList master = masterStates4.get( masterPos ); master.add( state ); } this.size++; } else { // all other Elements are stored into a list of 10-element weakReference // list. So if an Element gets lost (GCd), 10 states need to be replayed. final int thirdPos = size - secondaryPoolSize; final int masterPos = getMasterPos( thirdPos, tertiaryPoolFrequency ); if ( masterPos >= masterStates10.size() ) { final MasterList master = new MasterList( this, tertiaryPoolFrequency ); masterStates10.add( master ); master.add( state ); } else { final MasterList master = masterStates10.get( masterPos ); master.add( state ); } this.size++; } } protected void set( final int index, final InternalStorageState state ) { if ( index >= size ) { throw new IndexOutOfBoundsException(); } if ( index != state.getStorePosition() ) { throw new IllegalArgumentException(); } // the first 20 Elements are stored directly into an ArrayList if ( index < primaryPoolSize ) { final InternalStorageState o = primaryStates.get( index ); if ( o.isValidRestorePoint() ) { throw new IllegalArgumentException(); } primaryStates.set( index, state ); } else if ( index < secondaryPoolSize ) { // the next 100 Elements are stored into a list of 4-element weakReference // list. So if an Element gets lost (GCd), only 4 states need to be replayed. final int secPos = index - primaryPoolSize; final int masterPos = getMasterPos( secPos, secondaryPoolFrequency ); if ( masterPos >= masterStates4.size() ) { throw new IllegalStateException( "Replacing an existing entry must not generate a new list slot." ); } final MasterList master = masterStates4.get( masterPos ); final InternalStorageState o = master.getRaw( secPos ); if ( o != null && o.isValidRestorePoint() ) { throw new IllegalArgumentException(); } master.set( state, secPos ); } else { // all other Elements are stored into a list of 10-element weakReference // list. So if an Element gets lost (GCd), 10 states need to be replayed. final int thirdPos = index - secondaryPoolSize; final int masterPos = getMasterPos( thirdPos, tertiaryPoolFrequency ); if ( masterPos >= masterStates10.size() ) { throw new IllegalStateException( "Replacing an existing entry must not generate a new list slot." ); } else { final MasterList master = masterStates10.get( masterPos ); final InternalStorageState o = master.getRaw( thirdPos ); if ( o != null && o.isValidRestorePoint() ) { throw new IllegalArgumentException(); } master.set( state, thirdPos ); } } } /** * Removes all elements in the list. */ public void clear() { masterStates10.clear(); masterStates4.clear(); primaryStates.clear(); this.size = 0; } /** * Retrieves the element on position <code>index</code> in this list. * * @param index * the index. * @return the report state. */ public PageState get( final int index ) { if ( index >= size() || index < 0 ) { throw new IndexOutOfBoundsException( "Index is invalid. Index was " + index + "; size was " + size() ); } final InternalStorageState internal = getInternal( index ); if ( internal.isValidRestorePoint() == false ) { try { // From the 'internal' pagestate we know the worst case position where we can find a // page state to restore our report processing from. But maybe we have a cheaper solution // inbetween, maybe from previous restore runs. So we backtrack from the current target // to the worst case and start restoring from any earlier states. final int targetPageCursor = internal.getStorePosition(); final int stateCounter = internal.getLastSavePosition(); for ( int i = targetPageCursor - 1; i >= stateCounter; i -= 1 ) { final InternalStorageState startState = getInternal( i ); if ( startState.isValidRestorePoint() ) { final InternalStorageState internalStorageState = restoreState( targetPageCursor, i, startState ); return internalStorageState.getPageState(); } } throw new IllegalStateException(); } catch ( ReportProcessingException e ) { throw new IllegalStateException( e ); } } return internal.getPageState(); } /** * Internal handler function restore a state. Count denotes the number of pages required to be processed to restore * the page, when the reportstate master is used as source element. * * @param pageCursor * the page cursor for the end state of the restore sequence. * @param lastSaveState * the page cursor for the start state of the restore sequence. * @param rootstate * the root state. * @return the report state. * @throws org.pentaho.reporting.engine.classic.core.ReportProcessingException * if there was a problem processing the report. */ private InternalStorageState restoreState( final int pageCursor, final int lastSaveState, final InternalStorageState rootstate ) throws ReportProcessingException { logger.info( "Restoring global state " + pageCursor + " from " + lastSaveState ); if ( rootstate == null ) { throw new NullPointerException( "Master is null" ); } if ( rootstate.isValidRestorePoint() == false ) { throw new IllegalArgumentException(); } InternalStorageState state = rootstate; for ( int i = lastSaveState; i < pageCursor; i++ ) { final ReportProcessor pageProcess = getPageProcess(); final PageState pageState = pageProcess.processPage( state.getPageState(), false ); if ( pageState == null ) { throw new IllegalStateException( "State returned is null: Report processing reached premature end-point." ); } state = new InternalStorageState( pageState, i + 1, rootstate.getLastSavePosition() ); if ( pageState.isSafeToStoreEarly() ) { set( i + 1, state ); } } return state; } private InternalStorageState getInternal( int index ) { if ( index < primaryPoolSize ) { return primaryStates.get( index ); } else if ( index < secondaryPoolSize ) { index -= primaryPoolSize; final MasterList master = masterStates4.get( getMasterPos( index, secondaryPoolFrequency ) ); return (InternalStorageState) master.get( index ); } else { index -= secondaryPoolSize; final MasterList master = masterStates10.get( getMasterPos( index, tertiaryPoolFrequency ) ); return (InternalStorageState) master.get( index ); } } }