/*==========================================================================*\ | $Id: OdaResultSet.java,v 1.3 2012/05/09 14:34:43 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2012 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT 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 General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.reporter; import java.math.BigDecimal; import java.sql.Timestamp; import java.util.Enumeration; import org.webcat.oda.commons.IWebCATResultSet; import org.webcat.oda.commons.WebCATDataException; import org.webcat.woextensions.ReadOnlyEditingContext; import org.webcat.woextensions.WCFetchSpecification; import ognl.Ognl; import ognl.OgnlContext; import ognl.OgnlException; import ognl.enhance.ExpressionAccessor; import org.apache.log4j.Logger; import org.webcat.core.EOBase; import org.webcat.core.ObjectQuery; import org.webcat.core.QualifierUtils; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSTimestamp; import er.extensions.eof.ERXFetchSpecificationBatchIterator; //------------------------------------------------------------------------- /** * A result set for a report. * * @author Tony Allevato * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.3 $, $Date: 2012/05/09 14:34:43 $ */ public class OdaResultSet implements IWebCATResultSet { //~ Constructor ........................................................... // ---------------------------------------------------------- /** * Create a result set. * * @param dataSetId * The ID of the ReportDataSet for which this result set is being * generated * @param job * The ManagedReportGenerationJob that is generating the report that * will contain this data * @param query * The query defining this result set */ public OdaResultSet(int dataSetId, ManagedReportGenerationJob job, ObjectQuery query) { this.dataSetId = dataSetId; this.job = job; this.query = query; currentRow = 0; rawCurrentRow = 0; lastThrottleCheck = 0; currentBatchSize = 100; } //~ Public Methods ........................................................ // ---------------------------------------------------------- public void close() { editingContext.dispose(); // ReportGenerationTracker.getInstance().completeDataSetForJobId(jobId); job.completeCurrentTask(); } // ---------------------------------------------------------- public int currentRow() { return currentRow; } // ---------------------------------------------------------- public void execute() { recycleEditingContext(); EOQualifier qualifier = query.qualifier(editingContext); EOQualifier[] quals = QualifierUtils.partitionQualifier( qualifier, query.objectType()); fetchQualifier = quals[0]; inMemoryQualifier = EOBase.accessibleBy(job.user()).and(quals[1]); WCFetchSpecification<?> fetch = new WCFetchSpecification<EOEnterpriseObject>( query.objectType(), fetchQualifier, null); iterator = new ERXFetchSpecificationBatchIterator(fetch, editingContext); iterator.setBatchSize(currentBatchSize); // ReportGenerationTracker.getInstance().startNextDataSetForJobId(jobId, // iterator.count()); job.beginTask(null, iterator.count()); } // ---------------------------------------------------------- public void prepare(String entityType, String[] myExpressions) throws WebCATDataException { this.expressions = myExpressions; defaultContext = prepareOgnlContext(); accessors = new ExpressionAccessor[myExpressions.length]; int i = 0; for (String expression : myExpressions) { try { accessors[i] = Ognl.compileExpression( defaultContext, null, expression).getAccessor(); } catch (Exception e) { throw new WebCATDataException(e); } i++; } } // ---------------------------------------------------------- private OgnlContext prepareOgnlContext() { OgnlContext ctx = ReportUtilityEnvironment.newOgnlContext(); return ctx; } // ---------------------------------------------------------- public boolean moveToNextRow() { throttleIfNecessary(); boolean hasNext = true; rawCurrentRow++; long currentTime = System.currentTimeMillis(); if (currentTime - lastProgressUpdateTime >= TIME_BETWEEN_PROGRESS_UPDATES) { // ReportGenerationTracker tracker = // ReportGenerationTracker.getInstance(); // Check to see if the report has been canceled. If it has, we // just return no more rows here. if (job.isCancelled()) { return false; } /* if (!tracker.doesJobExistWithId(jobId)) { return false; }*/ job.worked(rawCurrentRow - rowCountAtLastProgressUpdate); // tracker.doWorkForJobId(jobId, // rawCurrentRow - rowCountAtLastProgressUpdate); lastProgressUpdateTime = currentTime; rowCountAtLastProgressUpdate = rawCurrentRow; } if (currentBatchEnum == null || !currentBatchEnum.hasMoreElements()) { hasNext = getNextBatch(); } if (hasNext) { currentObject = currentBatchEnum.nextElement(); currentRow++; } if (log.isDebugEnabled()) { // CAUTION: Since this row logging occurs on every row AND it is // possible that the toString() method for an object might // indirectly fetch other objects in order to display a human- // readable representation of itself, DEBUG level logging on this // class should only be enabled when absolutely necessary. String msg = "Row " + rawCurrentRow + ": " + currentObject.toString(); log.debug(msg); } return hasNext; } // ---------------------------------------------------------- public boolean booleanValueAtIndex(int column) throws WebCATDataException { Boolean value = evaluate(column, Boolean.class); return (value == null)? false : value; } // ---------------------------------------------------------- public BigDecimal decimalValueAtIndex(int column) throws WebCATDataException { return evaluate(column, BigDecimal.class); } // ---------------------------------------------------------- public double doubleValueAtIndex(int column) throws WebCATDataException { Double value = evaluate(column, Double.class); if (value == null) { return 0.0; } else { return value; } } // ---------------------------------------------------------- public int intValueAtIndex(int column) throws WebCATDataException { Integer value = evaluate(column, Integer.class); return (value == null)? 0 : value; } // ---------------------------------------------------------- public String stringValueAtIndex(int column) throws WebCATDataException { return evaluate(column, String.class); } // ---------------------------------------------------------- public Timestamp timestampValueAtIndex(int column) throws WebCATDataException { return evaluate(column, NSTimestamp.class); } // ---------------------------------------------------------- public boolean wasValueNull() { return wasNull; } //~ Private Methods ....................................................... // ---------------------------------------------------------- private void recycleEditingContext() { boolean suppressLog = false; if (editingContext != null) { suppressLog = editingContext.isLoggingSuppressed(); editingContext.dispose(); } editingContext = ReadOnlyEditingContext.newEditingContext(); editingContext.setSuppressesLogAfterFirstAttempt(true); editingContext.setLoggingSuppressed(suppressLog); if (iterator != null) { iterator.setEditingContext(editingContext); } } // ---------------------------------------------------------- private void updateMovingAverageAndRest() { batchTimeEnd = batchTimeStart; batchTimeStart = System.currentTimeMillis(); if (batchTimeEnd == 0) { // Bail out if this is the first time through. The batch size is // already set to a default value that we'll use to begin the // calculations. return; } // Note that this subtraction is "backwards" because of the way I've // reset the variables above. double avgTimePerRow = ((double) (batchTimeStart - batchTimeEnd)) / currentBatchSize; // Compute moving average. if (currentMovingAverage == 0) { currentMovingAverage = avgTimePerRow; } else { currentMovingAverage = ((MOVING_AVERAGE_WINDOW_SIZE - 1) * currentMovingAverage + avgTimePerRow) / MOVING_AVERAGE_WINDOW_SIZE; } //log.debug("Last_batch_size," + currentBatchSize + ",Last_batch_time," // + (batchTimeStart - batchTimeEnd) + ",Avg_time_per_row," // + avgTimePerRow + ",Current_moving_avg," + currentMovingAverage); // Compute the new batch size. long workTime = (long) (BATCH_TIME_SLICE * BATCH_LOAD_FACTOR); currentBatchSize = (int) (workTime / currentMovingAverage); if (currentBatchSize < BATCH_SIZE_MIN) { currentBatchSize = BATCH_SIZE_MIN; } else if (currentBatchSize > BATCH_SIZE_MAX) { currentBatchSize = BATCH_SIZE_MAX; } iterator.setBatchSize(currentBatchSize); try { long sleepTime = (long) (BATCH_TIME_SLICE * (1 - BATCH_LOAD_FACTOR)); Thread.sleep(sleepTime); } catch (InterruptedException e) { // Do nothing. } } // ---------------------------------------------------------- private boolean getNextBatch() { updateMovingAverageAndRest(); if (iterator.hasNextBatch()) { boolean getBatch = true; while (getBatch) { recycleEditingContext(); @SuppressWarnings("unchecked") NSArray<Object> nextBatch = iterator.nextBatch(); currentBatch = nextBatch; if (inMemoryQualifier != null) { currentBatch = EOQualifier.filteredArrayWithQualifier( currentBatch, inMemoryQualifier); } if (currentBatch.isEmpty()) { getBatch = iterator.hasNextBatch(); } else { currentBatchEnum = currentBatch.objectEnumerator(); return true; } } } return false; } // ---------------------------------------------------------- private void throttleIfNecessary() { long currentTime = System.currentTimeMillis(); if (currentTime - lastThrottleCheck > MILLIS_BETWEEN_THROTTLE_CHECK) { while (Reporter.getInstance().refreshThrottleStatus()) { try { Thread.sleep(MILLIS_TO_THROTTLE); } catch (InterruptedException e) { // Nothing to do } } lastThrottleCheck = System.currentTimeMillis(); } } // ---------------------------------------------------------- private <T> T evaluate(int column, Class<T> destType) throws WebCATDataException { ExpressionAccessor accessor = accessors[column]; Object result = null; defaultContext.setRoot(currentObject); try { result = accessor.get(defaultContext, currentObject); } catch (NullPointerException e) { // If any property along the key path evaluated to null, we don't // want to bail out; we'll just set the column value to null and // keep going. result = null; } catch (NSKeyValueCoding.UnknownKeyException e) { // Translate the expression into something a little easier for the // user to read. String msg = String.format( "In the expression above, the property \"%s\" is not " + "recognized by the source object (which is of type \"%s\")", e.key(), e.object().getClass().getName()); // FIXME fix this // ReportGenerationTracker rgt = // ReportGenerationTracker.getInstance(); // ReportDataSet dataSet = // ReportDataSet.forId(editingContext, dataSetId); // rgt.setLastErrorInfoForJobId(jobId, dataSet.name(), column, null, // expressions[column], msg); // Rethrow modified version of original exception with new msg NSKeyValueCoding.UnknownKeyException replacement = new NSKeyValueCoding.UnknownKeyException( msg, e.object(), e.key()); replacement.setStackTrace(e.getStackTrace()); throw new WebCATDataException(replacement); } catch (Exception e) { if (e instanceof OgnlException) { result = null; } else { // Before rethrowing the exception, pass the extra information // about where the error occurred to the report generation // tracker so that the queue processor can pass it along to // the user. This gets us better feedback than the standard // BIRT error message, which is something like "cannot get // value from column: N" with no other information. // FIXME fix this // ReportGenerationTracker rgt = // ReportGenerationTracker.getInstance(); // ReportDataSet dataSet = ReportDataSet.forId( // editingContext, dataSetId); // rgt.setLastErrorInfoForJobId(jobId, dataSet.name(), column, // null, expressions[column], e.getMessage()); throw new WebCATDataException(e); } } if (log.isDebugEnabled()) { log.debug(" Column " + column + " = " + result + ": " + expressions[column]); } if (result == null) { wasNull = true; return null; } else { wasNull = false; if (destType.isInstance(result)) { return destType.cast(result); } else { try { return destType.cast(defaultContext.getTypeConverter() .convertValue( null, null, null, null, result, destType)); } catch (Exception e) { return tryFallbackConversions(column, result, destType); } } } } // ---------------------------------------------------------- private <T> T tryFallbackConversions(int column, Object result, Class<T> destType) { if (destType == Integer.class) { // Conversion may have failed if we have a string that is a // floating point value and we want to convert to an integer. try { return destType.cast(Integer.valueOf( (int) Double.parseDouble(result.toString()))); } catch (NumberFormatException e) { // Fall through to the code below. } } else if (destType == NSTimestamp.class) { try { return destType.cast( new NSTimestamp(Long.parseLong(result.toString()))); } catch (NumberFormatException e) { // Fall through to the code below. } } String destinationType = destType.getSimpleName(); if (destType == BigDecimal.class) destinationType = "Decimal"; else if (destType == Double.class) destinationType = "Float"; else if (destType == NSTimestamp.class) destinationType = "Timestamp"; throw new IllegalArgumentException("The result (\"" + result.toString() + "\") of the expression [ " + expressions[column] + " ] could not be converted to type " + destinationType); } //~ Instance/static variables ............................................. @SuppressWarnings("unused") private int dataSetId; private ManagedReportGenerationJob job; private ObjectQuery query; private ReadOnlyEditingContext editingContext; private EOQualifier fetchQualifier; private EOQualifier inMemoryQualifier; private ERXFetchSpecificationBatchIterator iterator; private int currentRow; private int rawCurrentRow; private String[] expressions; private ExpressionAccessor[] accessors; private OgnlContext defaultContext; private NSArray<Object> currentBatch; private Enumeration<Object> currentBatchEnum; private Object currentObject; private boolean wasNull; private long lastThrottleCheck; private long batchTimeStart; private long batchTimeEnd; private int currentBatchSize; private double currentMovingAverage; private long lastProgressUpdateTime; private int rowCountAtLastProgressUpdate; private static final long TIME_BETWEEN_PROGRESS_UPDATES = 4000; private static final long MILLIS_BETWEEN_THROTTLE_CHECK = 3000; private static final long MILLIS_TO_THROTTLE = 5000; // TODO: Add these as Reporter subsystem configuration options; cache // their values when this object is created /* Ideal amount of time to spend on each batch */ private static final int BATCH_TIME_SLICE = 600; /* Fraction of the time slice to spend working (instead of resting) */ private static final float BATCH_LOAD_FACTOR = 0.75f; private static final int BATCH_SIZE_MIN = 25; private static final int BATCH_SIZE_MAX = 250; private static final int MOVING_AVERAGE_WINDOW_SIZE = 10; private static final Logger log = Logger.getLogger(OdaResultSet.class); }