/*==========================================================================*\ | $Id: designerPreview.java,v 1.3 2011/12/25 21:18:25 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2011 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.actions; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.StringReader; import java.util.Enumeration; import org.webcat.core.QualifierUtils; import org.webcat.core.objectquery.AdvancedQueryComparison; import org.webcat.core.objectquery.AdvancedQueryCriterion; import org.webcat.core.objectquery.AdvancedQueryModel; import org.webcat.core.objectquery.AdvancedQueryUtils; import org.webcat.reporter.ReportUtilityEnvironment; import org.webcat.woextensions.ReadOnlyEditingContext; import ognl.Node; import ognl.Ognl; import ognl.OgnlContext; import ognl.OgnlException; import ognl.enhance.ExpressionAccessor; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.appserver.WOSession; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EORelationship; import com.webobjects.eoaccess.EOUtilities; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOGenericRecord; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSTimestamp; import er.extensions.appserver.ERXDirectAction; import er.extensions.eof.ERXFetchSpecificationBatchIterator; import er.extensions.eof.ERXQ; //------------------------------------------------------------------------- /** * Direct action support for preview actions in the BIRT report designer. This * action is used in two phases, first by sending * "designerPreview/startRetrieval" to prep the retrieval and then * repeatedly sending "designerPreview/retrieveNextBatch" until the * response end-of-data marker is true. * * @author Tony Allevato * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.3 $, $Date: 2011/12/25 21:18:25 $ */ public class designerPreview extends ERXDirectAction { //~ Constructor ........................................................... // ---------------------------------------------------------- /** * Creates a new object. * @param request The incoming request */ public designerPreview(WORequest request) { super(request); } //~ Public Methods ........................................................ // ---------------------------------------------------------- public WOActionResults startRetrievalAction() { WOResponse response = new WOResponse(); WOSession session = session(); String entityType = request().formValueForKey(PARAM_ENTITY_TYPE).toString(); String expressionString = request().formValueForKey(PARAM_EXPRESSIONS).toString(); String[] expressions = expressionString.split("%===%"); int timeout = Integer.parseInt( request().formValueForKey(PARAM_TIMEOUT).toString()); ReadOnlyEditingContext context = ReadOnlyEditingContext.newEditingContext(); context.setSuppressesLogAfterFirstAttempt(true); EOQualifier fastQualifier = null; EOQualifier slowQualifier = null; if (request().formValueForKey(PARAM_QUERY) != null) { String query = request().formValueForKey(PARAM_QUERY).toString(); if (query.length() > 0) { EOQualifier fullQualifier = translateQueryToQualifier( entityType, query, context); EOQualifier[] quals = QualifierUtils.partitionQualifier( fullQualifier, entityType); fastQualifier = quals[0]; slowQualifier = quals[1]; } } try { OgnlContext ognlContext = ReportUtilityEnvironment.newOgnlContext(); EOEntity rootEntity = EOUtilities.entityNamed(context, entityType); EOFetchSpecification spec = new EOFetchSpecification( entityType, fastQualifier, null); ExpressionAccessor[] compiled = compileAndPrefetchExpressions( ognlContext, expressions, rootEntity, spec); ERXFetchSpecificationBatchIterator iterator = new ERXFetchSpecificationBatchIterator(spec, context); iterator.setBatchSize(50); session.setObjectForKey(iterator, SESSION_ITERATOR); session.setObjectForKey(compiled, SESSION_EXPRESSIONS); session.setObjectForKey(expressions, SESSION_EXPRESSION_STRINGS); session.setObjectForKey(timeout, SESSION_TIMEOUT); session.setObjectForKey(Boolean.FALSE, SESSION_CANCELED); if (fastQualifier == null) { session.removeObjectForKey(SESSION_FAST_QUALIFIER); } else { session.setObjectForKey(fastQualifier, SESSION_FAST_QUALIFIER); } if (slowQualifier == null) { session.removeObjectForKey(SESSION_SLOW_QUALIFIER); } else { session.setObjectForKey(slowQualifier, SESSION_SLOW_QUALIFIER); } session.setObjectForKey(System.currentTimeMillis(), SESSION_START_TIME); // A successful response to this action contains the session ID to // use to continue batching from this request. response.appendContentString(session.sessionID()); response.appendContentCharacter('\n'); } catch (Exception e) { // Send any exception back as the response so that the report // designer can report the error to the user. response = errorResponse(e); } return response; } // ---------------------------------------------------------- public WOActionResults retrieveNextBatchAction() { WOResponse response = new WOResponse(); try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); ERXFetchSpecificationBatchIterator iterator = (ERXFetchSpecificationBatchIterator)session().objectForKey( SESSION_ITERATOR); long startTime = (Long)session().objectForKey(SESSION_START_TIME); int timeout = (Integer)session().objectForKey(SESSION_TIMEOUT); long endTime; if(timeout == 0) { endTime = Long.MAX_VALUE; } else { endTime = startTime + (timeout * 1000); } int count = 0; boolean isTimedOut = (System.currentTimeMillis() > endTime); while (count < BATCH_SIZE && iterator.hasNextBatch() && !isCanceledInSession() && !isTimedOut) { int recordsRetrieved = serializeNextBatch(iterator, oos, endTime); count += recordsRetrieved; isTimedOut = (System.currentTimeMillis() > endTime); } // Write the end of data marker. oos.writeBoolean(false); oos.close(); baos.close(); NSData data = new NSData(baos.toByteArray()); response.appendContentData(data); } catch (Exception e) { response = errorResponse(e); } return response; } // ---------------------------------------------------------- public WOActionResults cancelRetrievalAction() { setCanceledInSession(); return new WOResponse(); } //~ Private Methods ....................................................... // ---------------------------------------------------------- private static WOResponse errorResponse(Exception e) { WOResponse response = new WOResponse(); response.appendContentString("!!! ERROR\n"); response.appendContentString(e.toString()); return response; } // ---------------------------------------------------------- private boolean isCanceledInSession() { synchronized (designerPreview.class) { return (Boolean)session().objectForKey(SESSION_CANCELED); } } // ---------------------------------------------------------- private void setCanceledInSession() { synchronized (designerPreview.class) { session().setObjectForKey(Boolean.TRUE, SESSION_CANCELED); } } // ---------------------------------------------------------- private int serializeNextBatch( ERXFetchSpecificationBatchIterator iterator, ObjectOutputStream oos, long endTime) throws IOException { WOSession session = session(); int recordsRetrieved = 0; if (iterator != null) { if (iterator.hasNextBatch()) { ExpressionAccessor[] expressions = (ExpressionAccessor[]) session.objectForKey(SESSION_EXPRESSIONS); String[] expressionStrings = (String[])session.objectForKey(SESSION_EXPRESSION_STRINGS); EOQualifier fastQualifier = (EOQualifier)session.objectForKey(SESSION_FAST_QUALIFIER); EOQualifier slowQualifier = (EOQualifier)session.objectForKey(SESSION_SLOW_QUALIFIER); EOQualifier q = null; if (slowQualifier != null && fastQualifier != null) { q = ERXQ.and(fastQualifier, slowQualifier); } else if (slowQualifier != null) { q = slowQualifier; } else if (fastQualifier != null) { q = fastQualifier; } @SuppressWarnings("unchecked") NSArray batch = EOQualifier.filteredArrayWithQualifier( iterator.nextBatch(), q); boolean isTimedOut = (System.currentTimeMillis() > endTime); Enumeration<?> e = batch.objectEnumerator(); while (e.hasMoreElements() && !isTimedOut) { // Write the next-object-exists marker. oos.writeBoolean(true); EOGenericRecord eo = (EOGenericRecord)e.nextElement(); for (int i = 0; i < expressions.length; i++) { OgnlContext ognlContext = ReportUtilityEnvironment.newOgnlContext(); Object value = getValueOfExpression(expressions[i], ognlContext, eo, expressionStrings[i]); if (value instanceof NSTimestamp) { // We don't want to send an NSTimestamp back to the // report designer because the report designer // doesn't have access to WebObjects classes. So, // we create a java.sql.Timestamp from its value // instead. NSTimestamp timestamp = (NSTimestamp)value; long time = timestamp.getTime(); java.sql.Timestamp sqlTime = new java.sql.Timestamp(time); value = sqlTime; } oos.writeObject(value); } recordsRetrieved++; isTimedOut = (System.currentTimeMillis() > endTime); } } // Recycle the editing context after we've processed this batch to // flush out all of the current objects. ReadOnlyEditingContext oldEC = (ReadOnlyEditingContext) iterator.editingContext(); boolean suppressLog = oldEC.isLoggingSuppressed(); oldEC.dispose(); ReadOnlyEditingContext newEC = ReadOnlyEditingContext.newEditingContext(); newEC.setSuppressesLogAfterFirstAttempt(true); newEC.setLoggingSuppressed(suppressLog); iterator.setEditingContext(newEC); } return recordsRetrieved; } // ---------------------------------------------------------- private ExpressionAccessor[] compileAndPrefetchExpressions( OgnlContext ognlContext, String[] expressions, EOEntity rootEntity, EOFetchSpecification spec) { ExpressionAccessor[] compiled = new ExpressionAccessor[expressions.length]; NSMutableArray<String> prefetchedRelationships = new NSMutableArray<String>(); int i = 0; for (String expression : expressions) { // We only bother prefetching relationships out of the expression // if some arbitrary prefix of the expression is a keypath and not // a richer OGNL expression. This allows us to still prefetch // relationships that are used in certain types of OGNL expressions // like selection or projection (for example, // "object.relationship.{? #this instanceof SomeClass }", but // without the challenge of trying to parse keypaths out of the // entire expression. EOEntity entity = rootEntity; String[] parts = expression.split("\\."); String partsSoFar = ""; for (String part : parts) { partsSoFar += part; EORelationship relationship = entity.relationshipNamed(part); if (relationship != null) { entity = relationship.destinationEntity(); prefetchedRelationships.addObject(partsSoFar); } else { break; } partsSoFar += "."; } // Compile the OGNL expressions for more efficient accesses during // the preview batch operations. If an error occurs, pass it back // up to the direct action so that a proper response can be sent // back to the report designer. Node node; try { node = Ognl.compileExpression(ognlContext, null, expression); compiled[i] = node.getAccessor(); } catch (Exception e) { throw new IllegalArgumentException(e); } i++; } spec.setPrefetchingRelationshipKeyPaths(prefetchedRelationships); return compiled; } // ---------------------------------------------------------- private Object getValueOfExpression( ExpressionAccessor accessor, OgnlContext ognlContext, EOGenericRecord object, String expressionString) { Object result = null; ognlContext.setRoot(object); try { result = accessor.get(ognlContext, object); } catch (NullPointerException e) { 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 (%s), the key \"%s\" is not recognized " + "by the source object (which is of type \"%s\")", expressionString, e.key(), e.object().getClass().getName()); throw new IllegalArgumentException(msg); } catch (Exception e) { if (e instanceof OgnlException) { // This hack is unsatisfactory, but it's really the only good // way to let null values in an OGNL expression that would // normally cause errors propagate out as a null column value // instead. if (e.getMessage().startsWith("source is null")) { return null; } else { log.error("Exception thrown while evaluating column " + "expression", e); throw new IllegalArgumentException(e); } } else { log.error("Exception thrown while evaluating column " + "expression", e); throw new IllegalArgumentException(e); } } return result; } // ---------------------------------------------------------- private EOQualifier translateQueryToQualifier( String entityType, String query, EOEditingContext ec) { AdvancedQueryModel model = new AdvancedQueryModel(); try { BufferedReader reader = new BufferedReader(new StringReader(query)); String line; NSMutableArray<AdvancedQueryCriterion> criteria = new NSMutableArray<AdvancedQueryCriterion>(); while ((line = reader.readLine()) != null) { String keypath = line; String comparisonString = reader.readLine(); String comparandTypeString = reader.readLine(); String valueRepresentation = reader.readLine(); reader.readLine(); AdvancedQueryCriterion criterion = new AdvancedQueryCriterion(); AdvancedQueryComparison comparison = AdvancedQueryComparison.comparisonWithName( comparisonString); criterion.setKeyPath(keypath); criterion.setComparison(comparison); criterion.setComparandType( Integer.parseInt(comparandTypeString)); int type = AdvancedQueryUtils.typeOfKeyPath( entityType, keypath); Object value = null; if (comparison == AdvancedQueryComparison.IS_BETWEEN || comparison == AdvancedQueryComparison.IS_NOT_BETWEEN) { value = AdvancedQueryUtils. valueRangeForPreviewRepresentation( type, valueRepresentation, ec); } else if (comparison.doesSupportMultipleValues()) { value = AdvancedQueryUtils. multipleValuesForPreviewRepresentation( type, valueRepresentation, ec); } else { value = AdvancedQueryUtils. singleValueForPreviewRepresentation( type, valueRepresentation, ec); } criterion.setValue(value); criteria.addObject(criterion); } model.setCriteria(criteria); return model.qualifierFromValues(); } catch (IOException e) { return null; } } //~ Instance/static variables ............................................. private static final int BATCH_SIZE = 50; private static final String PARAM_ENTITY_TYPE = "entityType"; private static final String PARAM_EXPRESSIONS = "expressions"; private static final String PARAM_QUERY = "query"; private static final String PARAM_TIMEOUT = "timeout"; private static final String SESSION_ITERATOR = "org.webcat.reporter.actions.designerPreview.iterator"; private static final String SESSION_EXPRESSIONS = "org.webcat.reporter.actions.designerPreview.expressions"; private static final String SESSION_EXPRESSION_STRINGS = "org.webcat.reporter.actions.designerPreview.expressionStrings"; private static final String SESSION_FAST_QUALIFIER = "org.webcat.reporter.actions.designerPreview.fastQualifier"; private static final String SESSION_SLOW_QUALIFIER = "org.webcat.reporter.actions.designerPreview.slowQualifier"; private static final String SESSION_TIMEOUT = "org.webcat.reporter.actions.designerPreview.timeout"; private static final String SESSION_START_TIME = "org.webcat.reporter.actions.designerPreview.startTime"; private static final String SESSION_CANCELED = "org.webcat.reporter.actions.designerPreview.canceled"; }