/******************************************************************************* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. *******************************************************************************/ package org.apache.ofbiz.content.survey; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import org.apache.ofbiz.base.location.FlexibleLocation; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.GeneralException; import org.apache.ofbiz.base.util.UtilMisc; import org.apache.ofbiz.base.util.UtilValidate; import org.apache.ofbiz.base.util.template.FreeMarkerWorker; import org.apache.ofbiz.entity.Delegator; import org.apache.ofbiz.entity.GenericEntityException; import org.apache.ofbiz.entity.GenericValue; import org.apache.ofbiz.entity.condition.EntityCondition; import org.apache.ofbiz.entity.condition.EntityOperator; import org.apache.ofbiz.entity.transaction.TransactionUtil; import org.apache.ofbiz.entity.util.EntityListIterator; import org.apache.ofbiz.entity.util.EntityQuery; import org.apache.ofbiz.entity.util.EntityUtil; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; /** * Survey Wrapper - Class to render survey forms */ public class SurveyWrapper { public static final String module = SurveyWrapper.class.getName(); protected Delegator delegator = null; protected String responseId = null; protected String partyId = null; protected String surveyId = null; protected Map<String, Object> templateContext = null; protected Map<String, Object> passThru = null; protected Map<String, Object> defaultValues = null; protected boolean edit = false; protected SurveyWrapper() {} public SurveyWrapper(Delegator delegator, String responseId, String partyId, String surveyId, Map<String, Object> passThru, Map<String, Object> defaultValues) { this.delegator = delegator; this.responseId = responseId; this.partyId = partyId; this.surveyId = surveyId; this.setPassThru(passThru); this.setDefaultValues(defaultValues); this.checkParameters(); } public SurveyWrapper(Delegator delegator, String responseId, String partyId, String surveyId, Map<String, Object> passThru) { this(delegator, responseId, partyId, surveyId, passThru, null); } public SurveyWrapper(Delegator delegator, String surveyId) { this(delegator, null, null, surveyId, null); } protected void checkParameters() { if (delegator == null || surveyId == null) { throw new IllegalArgumentException("Missing one or more required parameters (delegator, surveyId)"); } } /** * Sets the pass-thru values (hidden form fields) * @param passThru */ public void setPassThru(Map<String, Object> passThru) { if (passThru != null) { this.passThru = new HashMap<String, Object>(); this.passThru.putAll(passThru); } } /** * Sets the default values * @param defaultValues */ public void setDefaultValues(Map<String, Object> defaultValues) { if (defaultValues != null) { this.defaultValues = new HashMap<String, Object>(); this.defaultValues.putAll(defaultValues); } } /** * Adds an object to the FTL survey template context * @param name * @param value */ public void addToTemplateContext(String name, Object value) { if (templateContext == null) { templateContext = new HashMap<String, Object>(); } templateContext.put(name, value); } /** * Removes an object from the FTL survey template context * @param name */ public void removeFromTemplateContext(String name) { if (templateContext != null) templateContext.remove(name); } /** * Renders the Survey * @return Writer object from the parsed Freemarker Template * @throws SurveyWrapperException */ public Writer render(String templatePath) throws SurveyWrapperException { URL templateUrl = null; try { templateUrl = FlexibleLocation.resolveLocation(templatePath); } catch (MalformedURLException e) { throw new SurveyWrapperException(e); } if (templateUrl == null) { String errMsg = "Problem getting the template for Survey from URL: " + templatePath; Debug.logError(errMsg, module); throw new IllegalArgumentException(errMsg); } Writer writer = new StringWriter(); this.render(templateUrl, writer); return writer; } /** * Renders the Survey * @param templateUrl the template URL * @param writer the write * @throws SurveyWrapperException */ public void render(URL templateUrl, Writer writer) throws SurveyWrapperException { String responseId = this.getThisResponseId(); GenericValue survey = this.getSurvey(); List<GenericValue> surveyQuestionAndAppls = this.getSurveyQuestionAndAppls(); Map<String, Object> results = this.getResults(surveyQuestionAndAppls); Map<String, Object> currentAnswers = null; if (responseId != null && canUpdate()) { currentAnswers = this.getResponseAnswers(responseId); } else { currentAnswers = this.getResponseAnswers(null); } Map<String, Object> sqaaWithColIdListByMultiRespId = new HashMap<String, Object>(); for (GenericValue surveyQuestionAndAppl : surveyQuestionAndAppls) { String surveyMultiRespColId = surveyQuestionAndAppl.getString("surveyMultiRespColId"); if (UtilValidate.isNotEmpty(surveyMultiRespColId)) { String surveyMultiRespId = surveyQuestionAndAppl.getString("surveyMultiRespId"); UtilMisc.addToListInMap(surveyQuestionAndAppl, sqaaWithColIdListByMultiRespId, surveyMultiRespId); } } if (templateContext == null) { templateContext = new HashMap<String, Object>(); } templateContext.put("partyId", partyId); templateContext.put("survey", survey); templateContext.put("surveyResults", results); templateContext.put("surveyQuestionAndAppls", surveyQuestionAndAppls); templateContext.put("sqaaWithColIdListByMultiRespId", sqaaWithColIdListByMultiRespId); templateContext.put("alreadyShownSqaaPkWithColId", new HashSet()); templateContext.put("surveyAnswers", currentAnswers); templateContext.put("surveyResponseId", responseId); templateContext.put("sequenceSort", UtilMisc.toList("sequenceNum")); templateContext.put("additionalFields", passThru); templateContext.put("defaultValues", defaultValues); templateContext.put("delegator", this.delegator); templateContext.put("locale", Locale.getDefault()); Template template = this.getTemplate(templateUrl); try { FreeMarkerWorker.renderTemplate(template, templateContext, writer); } catch (TemplateException e) { Debug.logError(e, "Error rendering Survey with template at [" + templateUrl.toExternalForm() + "]", module); } catch (IOException e) { Debug.logError(e, "Error rendering Survey with template at [" + templateUrl.toExternalForm() + "]", module); } } // returns the FTL Template object // Note: the template will not be cached protected Template getTemplate(URL templateUrl) { Configuration config = FreeMarkerWorker.getDefaultOfbizConfig(); Template template = null; try { InputStream templateStream = templateUrl.openStream(); InputStreamReader templateReader = new InputStreamReader(templateStream); template = new Template(templateUrl.toExternalForm(), templateReader, config); } catch (IOException e) { Debug.logError(e, "Unable to get template from URL :" + templateUrl.toExternalForm(), module); } return template; } public void setEdit(boolean edit) { this.edit = edit; } // returns the GenericValue object for the current Survey public GenericValue getSurvey() { GenericValue survey = null; try { survey = EntityQuery.use(delegator).from("Survey").where("surveyId", surveyId).cache().queryOne(); } catch (GenericEntityException e) { Debug.logError(e, "Unable to get Survey : " + surveyId, module); } return survey; } public String getSurveyName() { GenericValue survey = this.getSurvey(); if (survey != null) { return survey.getString("surveyName"); } return ""; } // true if we can update this survey public boolean canUpdate() { if (this.edit) { return true; } GenericValue survey = this.getSurvey(); if (!"Y".equals(survey.getString("allowMultiple")) && !"Y".equals(survey.getString("allowUpdate"))) { return false; } return true; } public boolean canRespond() { String responseId = this.getThisResponseId(); if (responseId == null) { return true; } else { GenericValue survey = this.getSurvey(); if ("Y".equals(survey.getString("allowMultiple"))) { return true; } } return false; } // returns a list of SurveyQuestions (in order by sequence number) for the current Survey public List<GenericValue> getSurveyQuestionAndAppls() { List<GenericValue> questions = new LinkedList<GenericValue>(); try { questions = EntityQuery.use(delegator).from("SurveyQuestionAndAppl") .where("surveyId", surveyId) .orderBy("sequenceNum", "surveyMultiRespColId") .filterByDate().cache().queryList(); } catch (GenericEntityException e) { Debug.logError(e, "Unable to get questions for survey : " + surveyId, module); } return questions; } // returns the most current SurveyResponse ID for a survey; null if no party is found protected String getThisResponseId() { if (responseId != null) { return responseId; } if (partyId == null) { return null; } String responseId = null; List<GenericValue> responses = null; try { responses = EntityQuery.use(delegator).from("SurveyResponse") .where("surveyId", surveyId, "partyId", partyId) .orderBy("-lastModifiedDate") .queryList(); } catch (GenericEntityException e) { Debug.logError(e, module); } if (UtilValidate.isNotEmpty(responses)) { GenericValue response = EntityUtil.getFirst(responses); responseId = response.getString("surveyResponseId"); if (responses.size() > 1) { Debug.logWarning("More then one response found for survey : " + surveyId + " by party : " + partyId + " using most current", module); } } return responseId; } protected void setThisResponseId(String responseId) { this.responseId = responseId; } public long getNumberResponses() throws SurveyWrapperException { long responses = 0; try { responses = EntityQuery.use(delegator).from("SurveyResponse").where("surveyId", surveyId).queryCount(); } catch (GenericEntityException e) { throw new SurveyWrapperException(e); } return responses; } public List<GenericValue> getSurveyResponses(GenericValue question) throws SurveyWrapperException { List<GenericValue> responses = null; try { responses = EntityQuery.use(delegator).from("SurveyResponse").where("surveyQuestionId", question.get("surveyQuestionId")).queryList(); } catch (GenericEntityException e) { throw new SurveyWrapperException(e); } return responses; } // returns a Map of answers keyed on SurveyQuestion ID from the most current SurveyResponse ID public Map<String, Object> getResponseAnswers(String responseId) throws SurveyWrapperException { Map<String, Object> answerMap = new HashMap<String, Object>(); if (responseId != null) { List<GenericValue> answers = null; try { answers = EntityQuery.use(delegator).from("SurveyResponseAnswer").where("surveyResponseId", responseId).queryList(); } catch (GenericEntityException e) { Debug.logError(e, module); } if (UtilValidate.isNotEmpty(answers)) { for (GenericValue answer : answers) { answerMap.put(answer.getString("surveyQuestionId"), answer); } } } // get the pass-thru (posted form data) if (UtilValidate.isNotEmpty(passThru)) { for (String key : passThru.keySet()) { if (key.toUpperCase().startsWith("ANSWERS_")) { int splitIndex = key.indexOf('_'); String questionId = key.substring(splitIndex+1); Map<String, Object> thisAnswer = new HashMap<String, Object>(); String answer = (String) passThru.remove(key); thisAnswer.put("booleanResponse", answer); thisAnswer.put("currencyResponse", answer); thisAnswer.put("floatResponse", answer); thisAnswer.put("numericResponse", answer); thisAnswer.put("textResponse", answer); thisAnswer.put("surveyOptionSeqId", answer); // this is okay since only one will be looked at answerMap.put(questionId, thisAnswer); } } } return answerMap; } public List<GenericValue> getQuestionResponses(GenericValue question, int startIndex, int number) throws SurveyWrapperException { List<GenericValue> resp = null; boolean beganTransaction = false; try { beganTransaction = TransactionUtil.begin(); int maxRows = startIndex + number; EntityListIterator eli = this.getEli(question, maxRows); if (startIndex > 0 && number > 0) { resp = eli.getPartialList(startIndex, number); } else { resp = eli.getCompleteList(); } eli.close(); } catch (GenericEntityException e) { try { // only rollback the transaction if we started one... TransactionUtil.rollback(beganTransaction, "Error getting survey question responses", e); } catch (GenericEntityException e2) { Debug.logError(e2, "Could not rollback transaction: " + e2.toString(), module); } throw new SurveyWrapperException(e); } finally { try { // only commit the transaction if we started one... TransactionUtil.commit(beganTransaction); } catch (GenericEntityException e) { throw new SurveyWrapperException(e); } } return resp; } public Map<String, Object> getResults(List<GenericValue> questions) throws SurveyWrapperException { Map<String, Object> questionResults = new HashMap<String, Object>(); if (questions != null) { for (GenericValue question : questions) { Map<String, Object> results = getResultInfo(question); if (results != null) { questionResults.put(question.getString("surveyQuestionId"), results); } } } return questionResults; } // returns a map of question reqsults public Map<String, Object> getResultInfo(GenericValue question) throws SurveyWrapperException { Map<String, Object> resultMap = new HashMap<String, Object>(); // special keys in the result: // "_q_type" - question type (SurveyQuestionTypeId) // "_a_type" - answer type ("boolean", "option", "long", "double", "text") // "_total" - number of total responses (all types) // "_tally" - tally of all response values (number types) // "_average" - average of all response values (number types) // "_yes_total" - number of 'Y' (true) reponses (boolean type) // "_no_total" - number of 'N' (false) responses (boolean type) // "_yes_percent" - number of 'Y' (true) reponses (boolean type) // "_no_percent" - number of 'N' (false) responses (boolean type) // [optionId] - Map containing '_total, _percent' keys (option type) String questionType = question.getString("surveyQuestionTypeId"); resultMap.put("_q_type", questionType); // call the proper method based on the question type // note this will need to be updated as new types are added if ("OPTION".equals(questionType)) { Map<String, Object> thisResult = getOptionResult(question); if (thisResult != null) { Long questionTotal = (Long) thisResult.remove("_total"); if (questionTotal == null) { questionTotal = Long.valueOf(0); } // set the total responses resultMap.put("_total", questionTotal); // create the map of option info ("_total", "_percent") for (String optId : thisResult.keySet()) { Map<String, Object> optMap = new HashMap<String, Object>(); Long optTotal = (Long) thisResult.get(optId); if (optTotal == null) { optTotal = Long.valueOf(0); } Long percent = Long.valueOf((long)(((double)optTotal.longValue() / (double)questionTotal.longValue()) * 100)); optMap.put("_total", optTotal); optMap.put("_percent", percent); resultMap.put(optId, optMap); } resultMap.put("_a_type", "option"); } } else if ("BOOLEAN".equals(questionType)) { long[] thisResult = getBooleanResult(question); long yesPercent = thisResult[1] > 0 ? (long)(((double)thisResult[1] / (double)thisResult[0]) * 100) : 0; long noPercent = thisResult[2] > 0 ? (long)(((double)thisResult[2] / (double)thisResult[0]) * 100) : 0; resultMap.put("_total", Long.valueOf(thisResult[0])); resultMap.put("_yes_total", Long.valueOf(thisResult[1])); resultMap.put("_no_total", Long.valueOf(thisResult[2])); resultMap.put("_yes_percent", Long.valueOf(yesPercent)); resultMap.put("_no_percent", Long.valueOf(noPercent)); resultMap.put("_a_type", "boolean"); } else if ("NUMBER_LONG".equals(questionType)) { double[] thisResult = getNumberResult(question, 1); resultMap.put("_total", Long.valueOf((long)thisResult[0])); resultMap.put("_tally", Long.valueOf((long)thisResult[1])); resultMap.put("_average", Long.valueOf((long)thisResult[2])); resultMap.put("_a_type", "long"); } else if ("NUMBER_CURRENCY".equals(questionType)) { double[] thisResult = getNumberResult(question, 2); resultMap.put("_total", Long.valueOf((long)thisResult[0])); resultMap.put("_tally", Double.valueOf(thisResult[1])); resultMap.put("_average", Double.valueOf(thisResult[2])); resultMap.put("_a_type", "double"); } else if ("NUMBER_FLOAT".equals(questionType)) { double[] thisResult = getNumberResult(question, 3); resultMap.put("_total", Long.valueOf((long)thisResult[0])); resultMap.put("_tally", Double.valueOf(thisResult[1])); resultMap.put("_average", Double.valueOf(thisResult[2])); resultMap.put("_a_type", "double"); } else if ("SEPERATOR_LINE".equals(questionType) || "SEPERATOR_TEXT".equals(questionType)) { // not really a question; ingore completely return null; } else { // default is text resultMap.put("_total", Long.valueOf(getTextResult(question))); resultMap.put("_a_type", "text"); } return resultMap; } private long[] getBooleanResult(GenericValue question) throws SurveyWrapperException { boolean beganTransaction = false; try { beganTransaction = TransactionUtil.begin(); long[] result = { 0, 0, 0 }; // index 0 = total responses // index 1 = total yes // index 2 = total no EntityListIterator eli = this.getEli(question, -1); if (eli != null) { GenericValue value; while (((value = eli.next()) != null)) { if ("Y".equalsIgnoreCase(value.getString("booleanResponse"))) { result[1]++; } else { result[2]++; } result[0]++; // increment the count } eli.close(); } return result; } catch (GenericEntityException e) { try { // only rollback the transaction if we started one... TransactionUtil.rollback(beganTransaction, "Error getting survey question responses Boolean result", e); } catch (GenericEntityException e2) { Debug.logError(e2, "Could not rollback transaction: " + e2.toString(), module); } throw new SurveyWrapperException(e); } finally { try { // only commit the transaction if we started one... TransactionUtil.commit(beganTransaction); } catch (GenericEntityException e) { throw new SurveyWrapperException(e); } } } private double[] getNumberResult(GenericValue question, int type) throws SurveyWrapperException { double[] result = { 0, 0, 0 }; // index 0 = total responses // index 1 = tally // index 2 = average boolean beganTransaction = false; try { beganTransaction = TransactionUtil.begin(); EntityListIterator eli = this.getEli(question, -1); if (eli != null) { GenericValue value; while (((value = eli.next()) != null)) { switch (type) { case 1: Long n = value.getLong("numericResponse"); if (UtilValidate.isNotEmpty(n)) { result[1] += n.longValue(); } break; case 2: Double c = value.getDouble("currencyResponse"); if (UtilValidate.isNotEmpty(c)) { result[1] += (((double) Math.round((c.doubleValue() - c.doubleValue()) * 100)) / 100); } break; case 3: Double f = value.getDouble("floatResponse"); if (UtilValidate.isNotEmpty(f)) { result[1] += f.doubleValue(); } break; } result[0]++; // increment the count } eli.close(); } } catch (GenericEntityException e) { try { // only rollback the transaction if we started one... TransactionUtil.rollback(beganTransaction, "Error getting survey question responses Number result", e); } catch (GenericEntityException e2) { Debug.logError(e2, "Could not rollback transaction: " + e2.toString(), module); } throw new SurveyWrapperException(e); } finally { try { // only commit the transaction if we started one... TransactionUtil.commit(beganTransaction); } catch (GenericEntityException e) { throw new SurveyWrapperException(e); } } // average switch (type) { case 1: if (result[0] > 0) result[2] = ((long) result[1]) / ((long) result[0]); break; case 2: if (result[0] > 0) result[2] = (((double) Math.round((result[1] / result[0]) * 100)) / 100); break; case 3: if (result[0] > 0) result[2] = result[1] / result[0]; break; } return result; } private long getTextResult(GenericValue question) throws SurveyWrapperException { long result = 0; try { result = EntityQuery.use(delegator).from("SurveyResponseAndAnswer") .where(makeEliCondition(question)) .queryCount(); } catch (GenericEntityException e) { Debug.logError(e, module); throw new SurveyWrapperException("Unable to get responses", e); } return result; } private Map<String, Object> getOptionResult(GenericValue question) throws SurveyWrapperException { Map<String, Object> result = new HashMap<String, Object>(); long total = 0; boolean beganTransaction = false; try { beganTransaction = TransactionUtil.begin(); EntityListIterator eli = this.getEli(question, -1); if (eli != null) { GenericValue value; while (((value = eli.next()) != null)) { String optionId = value.getString("surveyOptionSeqId"); if (UtilValidate.isNotEmpty(optionId)) { Long optCount = (Long) result.remove(optionId); if (optCount == null) { optCount = Long.valueOf(1); } else { optCount = Long.valueOf(1 + optCount.longValue()); } result.put(optionId, optCount); total++; // increment the count } } eli.close(); } } catch (GenericEntityException e) { try { // only rollback the transaction if we started one... TransactionUtil.rollback(beganTransaction, "Error getting survey question responses Option result", e); } catch (GenericEntityException e2) { Debug.logError(e2, "Could not rollback transaction: " + e2.toString(), module); } throw new SurveyWrapperException(e); } finally { try { // only commit the transaction if we started one... TransactionUtil.commit(beganTransaction); } catch (GenericEntityException e) { throw new SurveyWrapperException(e); } } result.put("_total", Long.valueOf(total)); return result; } private EntityCondition makeEliCondition(GenericValue question) { return EntityCondition.makeCondition(UtilMisc.toList(EntityCondition.makeCondition("surveyQuestionId", EntityOperator.EQUALS, question.getString("surveyQuestionId")), EntityCondition.makeCondition("surveyId", EntityOperator.EQUALS, surveyId)), EntityOperator.AND); } private EntityListIterator getEli(GenericValue question, int maxRows) throws GenericEntityException { return EntityQuery.use(delegator).from("SurveyResponseAndAnswer") .where(makeEliCondition(question)) .cursorScrollInsensitive() .maxRows(maxRows) .queryIterator(); } @SuppressWarnings("serial") protected class SurveyWrapperException extends GeneralException { public SurveyWrapperException() { super(); } public SurveyWrapperException(String str) { super(str); } public SurveyWrapperException(String str, Throwable nested) { super(str, nested); } public SurveyWrapperException(Throwable nested) { super(nested); } } }