DataExportUtil.java

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.reporting.export;

import java.io.File;
import java.io.PrintWriter;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.WeakHashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.log.CommonsLogLogChute;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.app.event.EventCartridge;
import org.apache.velocity.app.event.MethodExceptionEventHandler;
import org.openmrs.Cohort;
import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;
import org.openmrs.report.EvaluationContext;
import org.openmrs.util.OpenmrsUtil;

/**
 * Utility methods for use by Data Exports
 * 
 * @deprecated see reportingcompatibility module
 */
@Deprecated
public class DataExportUtil {
	
	private static Map<String, Object> dataExportKeys = new WeakHashMap<String, Object>();
	
	/**
	 * Allows a module or some other service to add things to the available keys in the velocity
	 * context
	 * 
	 * @see #generateExport(DataExportReportObject, Cohort, DataExportFunctions, EvaluationContext)
	 */
	public static void putDataExportKey(String key, Object obj) {
		dataExportKeys.put(key, obj);
	}
	
	/**
	 * Remove the given key from the available data export keys If the key doesn't exist, this will
	 * fail silently
	 * 
	 * @param key key to remove
	 * @see #putDataExportKey(String, Object)
	 * @see #generateExport(DataExportReportObject, Cohort, DataExportFunctions, EvaluationContext)
	 */
	public static void removeDataExportKey(String key) {
		dataExportKeys.remove(key);
	}
	
	/**
	 * Find the data export key previously added or null if not found
	 * 
	 * @param key
	 * @return Object the Data Export Key with the key identifier. Returns null if not found
	 * @see #putDataExportKey(String, Object)
	 * @see #generateExport(DataExportReportObject, Cohort, DataExportFunctions, EvaluationContext)
	 */
	public static Object getDataExportKey(String key) {
		return dataExportKeys.get(key);
	}
	
	/**
	 * @param exports
	 */
	public static void generateExports(List<DataExportReportObject> exports, EvaluationContext context) {
		
		Log log = LogFactory.getLog(DataExportUtil.class);
		
		for (DataExportReportObject dataExport : exports) {
			try {
				generateExport(dataExport, null, context);
			}
			catch (Exception e) {
				log.warn("Error while generating export: " + dataExport, e);
			}
		}
		
	}
	
	/**
	 * Generates a data export file given a data export (columns) and patient set (rows).
	 * 
	 * @param dataExport
	 * @param patientSet
	 * @param separator
	 * @throws Exception
	 */
	public static void generateExport(DataExportReportObject dataExport, Cohort patientSet, String separator,
	        EvaluationContext context) throws Exception {
		// Set up functions used in the report ( $!{fn:...} )
		DataExportFunctions functions = new DataExportFunctions();
		functions.setSeparator(separator);
		generateExport(dataExport, patientSet, functions, context);
	}
	
	/**
	 * @param dataExport
	 * @param patientSet
	 * @throws Exception
	 */
	public static void generateExport(DataExportReportObject dataExport, Cohort patientSet, EvaluationContext context)
	        throws Exception {
		// Set up functions used in the report ( $!{fn:...} )
		DataExportFunctions functions = new DataExportFunctions();
		generateExport(dataExport, patientSet, functions, context);
	}
	
	/**
	 * Auto generated method comment
	 * 
	 * @param dataExport
	 * @param patientSet
	 * @param functions
	 * @param context
	 * @throws Exception
	 */
	public static void generateExport(DataExportReportObject dataExport, Cohort patientSet, DataExportFunctions functions,
	        EvaluationContext context) throws Exception {
		
		// defining log file here to attempt to reduce memory consumption
		Log log = LogFactory.getLog(DataExportUtil.class);
		
		VelocityEngine velocityEngine = new VelocityEngine();
		
		velocityEngine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
		    "org.apache.velocity.runtime.log.CommonsLogLogChute");
		velocityEngine.setProperty(CommonsLogLogChute.LOGCHUTE_COMMONS_LOG_NAME, "dataexport_velocity");
		
		try {
			velocityEngine.init();
		}
		catch (Exception e) {
			log.error("Error initializing Velocity engine", e);
		}
		
		File file = getGeneratedFile(dataExport);
		PrintWriter report = new PrintWriter(file);
		
		VelocityContext velocityContext = new VelocityContext();
		
		// Set up list of patients if one wasn't passed into this method
		if (patientSet == null) {
			patientSet = dataExport.generatePatientSet(context);
			functions.setAllPatients(dataExport.isAllPatients());
		}
		
		// add the error handler
		EventCartridge ec = new EventCartridge();
		ec.addEventHandler(new VelocityExceptionHandler());
		velocityContext.attachEventCartridge(ec);
		
		// Set up velocity utils
		Locale locale = Context.getLocale();
		velocityContext.put("locale", locale);
		velocityContext.put("fn", functions);
		
		/*
		 * If we have any additional velocity objects that need to 
		 * be added, do so here.
		 */
		if (dataExportKeys != null && dataExportKeys.size() != 0) {
			for (Map.Entry<String, Object> entry : dataExportKeys.entrySet()) {
				velocityContext.put(entry.getKey(), entry.getValue());
			}
		}
		
		velocityContext.put("patientSet", patientSet);
		
		String template = dataExport.generateTemplate();
		
		// check if some deprecated columns are being used in this export
		// warning: hacky.
		if (template.contains("fn.getPatientAttr('Patient', 'tribe')")) {
			throw new APIException(
			        "Unable to generate export: "
			                + dataExport.getName()
			                + " because it contains a reference to an outdated 'tribe' column.  You must install the 'Tribe Module' into OpenMRS to continue to reference tribes in OpenMRS.");
		}
		
		if (log.isDebugEnabled())
			log.debug("Template: " + template.substring(0, template.length() < 3500 ? template.length() : 3500) + "...");
		
		try {
			velocityEngine.evaluate(velocityContext, report, DataExportUtil.class.getName(), template);
		}
		catch (Exception e) {
			log.error("Error evaluating data export " + dataExport.getReportObjectId(), e);
			log.error("Template: " + template.substring(0, template.length() < 3500 ? template.length() : 3500) + "...");
			report.print("\n\nError: \n" + e.toString() + "\n Stacktrace: \n");
			e.printStackTrace(report);
		}
		finally {
			report.close();
			velocityContext.remove("fn");
			velocityContext.remove("patientSet");
			velocityContext = null;
			
			// reset the ParserPool to something else now?
			// using this to get to RuntimeInstance.init();
			velocityEngine.init();
			
			velocityEngine = null;
			
			patientSet = null;
			functions.clear();
			functions = null;
			template = null;
			dataExport = null;
			log.debug("Clearing hibernate session");
			Context.clearSession();
			
			// clear out the excess objects
			System.gc();
			System.gc();
		}
		
	}
	
	/**
	 * Returns the path and name of the generated file
	 * 
	 * @param dataExport
	 */
	public static File getGeneratedFile(DataExportReportObject dataExport) {
		File dir = new File(OpenmrsUtil.getApplicationDataDirectory(), "dataExports");
		dir.mkdirs();
		
		String filename = dataExport.getName().replace(" ", "_");
		filename += "_" + Context.getLocale().toString().toLowerCase();
		
		File file = new File(dir, filename);
		
		return file;
	}
	
	/**
	 * Private class used for velocity error masking
	 */
	public static class VelocityExceptionHandler implements MethodExceptionEventHandler {
		
		private Log log = LogFactory.getLog(this.getClass());
		
		/**
		 * When a user-supplied method throws an exception, the MethodExceptionEventHandler is
		 * invoked with the Class, method name and thrown Exception. The handler can either return a
		 * valid Object to be used as the return value of the method call, or throw the passed-in or
		 * new Exception, which will be wrapped and propogated to the user as a
		 * MethodInvocationException
		 * 
		 * @see org.apache.velocity.app.event.MethodExceptionEventHandler#methodException(java.lang.Class,
		 *      java.lang.String, java.lang.Exception)
		 */
		@SuppressWarnings("unchecked")
		public Object methodException(Class claz, String method, Exception e) throws Exception {
			
			log.debug("Claz: " + claz.getName() + " method: " + method, e);
			
			// if formatting a date (and probably getting an "IllegalArguementException")
			if ("format".equals(method))
				return null;
			
			// keep the default behaviour
			throw e;
		}
		
	}
	
}