/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/assignment/trunk/assignment-impl/impl/src/java/org/sakaiproject/assignment/impl/conversion/impl/CombineDuplicateSubmissionsConversionHandler.java $
* $Id: CombineDuplicateSubmissionsConversionHandler.java 105182 2012-02-27 20:23:29Z zqian@umich.edu $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009 The Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.assignment.impl.conversion.impl;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.util.conversion.SchemaConversionHandler;
import org.sakaiproject.entity.cover.EntityManager;
import org.apache.commons.codec.binary.Base64;
public class CombineDuplicateSubmissionsConversionHandler implements SchemaConversionHandler
{
private static final Log log = LogFactory.getLog(CombineDuplicateSubmissionsConversionHandler.class);
// db driver
private String m_dbDriver = null;
/**
* {@inheritDoc}
*/
public String getDbDriver()
{
return m_dbDriver;
}
/**
* {@inheritDoc}
*/
public void setDbDriver(String dbDriver)
{
m_dbDriver = dbDriver;
}
public boolean convertSource(String id, Object source, PreparedStatement updateRecord) throws SQLException
{
List<String> xml = (List<String>) source;
SortedSet<String> identifiers = new TreeSet<String>();
List<AssignmentSubmissionAccess> saxlist = new ArrayList<AssignmentSubmissionAccess>();
for(int i = 0; i < xml.size(); i++)
{
AssignmentSubmissionAccess sax = new AssignmentSubmissionAccess();
saxlist.add(sax);
try
{
sax.parse(xml.get(i));
identifiers.add(sax.getId());
}
catch (Exception e1)
{
log.warn("Failed to parse " + id + "[" + xml + "]", e1);
// return false;
}
}
for(int i = saxlist.size() - 1; i > 0; i--)
{
saxlist.set(i - 1, combineItems(saxlist.get(i), saxlist.get(i - 1)));
}
if (saxlist.size() > 0) {
AssignmentSubmissionAccess result = saxlist.get(0);
String xml0 = result.toXml();
String submitTime0 = result.getDatesubmitted();
String submitted0 = result.getSubmitted();
String graded0 = result.getGraded();
String id0 = result.getId();
log.info("updating \"" + id0 + " revising XML");
if (getDbDriver().indexOf("mysql") != -1)
{
// see http://bugs.sakaiproject.org/jira/browse/SAK-1737
// MySQL setCharacterStream() is broken and truncates UTF-8
// international characters sometimes. So use setBytes()
// instead (just for MySQL).
try
{
updateRecord.setBytes(1, xml0.getBytes("UTF-8"));
}
catch (UnsupportedEncodingException e)
{
log.info(e.getMessage() + xml0);
}
}
else
{
updateRecord.setCharacterStream(1, new StringReader(xml0), xml0.length());
}
updateRecord.setString(2, submitTime0);
updateRecord.setString(3, submitted0);
updateRecord.setString(4, graded0);
updateRecord.setString(5, id0);
return true;
}
else {
return false;
}
}
protected AssignmentSubmissionAccess combineItems(AssignmentSubmissionAccess item1, AssignmentSubmissionAccess item2)
{
AssignmentSubmissionAccess keepItem=item1;
AssignmentSubmissionAccess removeItem=item2;
boolean usePreviousRecords = false;
// for normal assignment
//it is student-generated (submitted==TRUE && dateSubmittted==SOME_TIMESTAMP) or submitted=false),
//or it is instructor generated (submitted==TRUE && dateSubmitted==null)
if("true".equals(item1.getSubmitted()) && item1.getDatesubmitted() != null
&& !("true".equals(item2.getSubmitted()) && item2.getDatesubmitted() != null))
{
// item1 is student submission
keepItem = item1;
removeItem = item2;
}
else if("true".equals(item2.getSubmitted()) && item2.getDatesubmitted() != null
&& !("true".equals(item1.getSubmitted()) && item1.getDatesubmitted() != null))
{
// item2 is student submission
keepItem = item2;
removeItem = item1;
}
else if("true".equals(item2.getSubmitted()) && item2.getDatesubmitted() != null
&& ("true".equals(item1.getSubmitted()) && item1.getDatesubmitted() != null))
{
// both are valid in terms of submission status and submit date
Integer t1 = getIntegerObject(item1.getDatesubmitted());
Integer t2 = getIntegerObject(item2.getDatesubmitted());
if (t1 != null && t2 != null)
{
String grade1= StringUtils.trimToNull(item1.getGrade());
String grade2 = StringUtils.trimToNull(item2.getGrade());
if (item1.getGradereleased().equalsIgnoreCase(Boolean.TRUE.toString()) && item2.getGradereleased().equalsIgnoreCase(Boolean.TRUE.toString()))
{
// if both grades has been released
if (nonDefaultGrade(grade1) && nonDefaultGrade(grade2) && !grade1.equals(grade2))
{
// if both grades are release and are different, set the previous one as previous grade and keep the later one as the current grade
// to-do: don't have a good way to modify all previous properties right now
usePreviousRecords = true;
if (t1.intValue() < t2.intValue())
{
// keep the later one
keepItem = item2;
removeItem = item1;
}
else
{
keepItem = item1;
removeItem = item2;
}
}
else if (nonDefaultGrade(grade1) && !nonDefaultGrade(grade2))
{
keepItem = item1;
removeItem = item2;
}
else if (!nonDefaultGrade(grade1) && nonDefaultGrade(grade2))
{
keepItem = item2;
removeItem = item1;
}
else
{
// both are default grade
}
}
else if (item1.getGradereleased().equalsIgnoreCase(Boolean.TRUE.toString()))
{
// keep the released grade one
keepItem = item1;
removeItem = item2;
}
else if (item2.getGradereleased().equalsIgnoreCase(Boolean.TRUE.toString()))
{
// keep the released grade one
keepItem = item2;
removeItem = item1;
}
else
{
// neither been released
}
}
else if (t1 != null)
{
// keep whichever is not null
keepItem = item1;
removeItem = item2;
}
else
{
// keep whichever is not null
keepItem = item2;
removeItem = item1;
}
}
else
{
// if there is no student submission, just duplicate instructor record
if (StringUtils.trimToNull(item1.getFeedbacktext()) != null || StringUtils.trimToNull(item1.getFeedbackcomment()) != null || StringUtils.trimToNull(item1.getGrade()) != null)
{
// item 1 has some grading data
keepItem = item1;
removeItem = item2;
}
else if (StringUtils.trimToNull(item2.getFeedbacktext()) != null || StringUtils.trimToNull(item2.getFeedbackcomment()) != null || StringUtils.trimToNull(item2.getGrade()) != null)
{
// item 2 has some grading data
keepItem = item2;
removeItem = item1;
}
else
{
// if none of them contains useful information, randomly pick one to keep
//keepItem = item1;
//removeItem = item2;
}
}
// need to verify whether student or instructor data
// takes precedence if both exist
if(keepItem.getDatereturned() == null && removeItem.getDatereturned() != null)
{
keepItem.setDatereturned(removeItem.getDatereturned());
}
if(keepItem.getDatesubmitted() == null && removeItem.getDatesubmitted() != null)
{
keepItem.setDatesubmitted(removeItem.getDatesubmitted());
}
// in case we need to update the previous gradeing info inside properties
if (usePreviousRecords)
{
log.info("need to update previous grading information keepItem id=" + keepItem.getId() + " removeItem id=" + removeItem.getId());
Map<String, Object> propertiesMap = keepItem.saxSerializableProperties.getSerializableProperties();
// the properties definition copied from ResourceProperties.java file
/** Property for assignment submission's previous grade (user settable). [String] */
String PROP_SUBMISSION_PREVIOUS_GRADES = "CHEF:submission_previous_grades";
/** Property for assignment submission's scaled previous grade (user settable). [String] */
String PROP_SUBMISSION_SCALED_PREVIOUS_GRADES = "CHEF:submission_scaled_previous_grades";
/** Property for assignment submission's previous inline feedback text (user settable). [String] */
String PROP_SUBMISSION_PREVIOUS_FEEDBACK_TEXT = "CHEF:submission_previous_feedback_text";
/** Property for assignment submission's previous feedback comment (user settable). [String] */
String PROP_SUBMISSION_PREVIOUS_FEEDBACK_COMMENT = "CHEF:submission_previous_feedback_comment";
// the properties definition from AssignmentAction.java
/** property for previous feedback attachments **/
String PROP_SUBMISSION_PREVIOUS_FEEDBACK_ATTACHMENTS = "prop_submission_previous_feedback_attachments";
String previousGrades = combineGrades((String) propertiesMap.get(PROP_SUBMISSION_PREVIOUS_GRADES), removeItem.getGrade(), "graded on " + removeItem.getDatereturned());
if (previousGrades != null)
{
propertiesMap.put(PROP_SUBMISSION_PREVIOUS_GRADES, previousGrades);
}
String previousFeedbackText = combinePropertyWithText((String) propertiesMap.get(PROP_SUBMISSION_PREVIOUS_FEEDBACK_TEXT), removeItem.getFeedbacktext(), "graded on " + removeItem.getDatereturned());
if (previousFeedbackText != null)
{
propertiesMap.put(PROP_SUBMISSION_PREVIOUS_FEEDBACK_TEXT, previousFeedbackText);
}
String previousFeedbackComment = combinePropertyWithText((String) propertiesMap.get(PROP_SUBMISSION_PREVIOUS_FEEDBACK_COMMENT), removeItem.getFeedbackcomment(), "graded on " + removeItem.getDatereturned());
if (previousFeedbackComment != null)
{
propertiesMap.put(PROP_SUBMISSION_PREVIOUS_FEEDBACK_COMMENT, previousFeedbackComment);
}
String previousAttachments = (String) propertiesMap.get(PROP_SUBMISSION_PREVIOUS_FEEDBACK_COMMENT);
List<String> attachments = removeItem.getFeedbackattachments();
if (attachments != null && attachments.size() > 0)
{
if (previousAttachments == null)
{
previousAttachments = "";
}
// add the attachments
for (int k =0; k < attachments.size(); k++)
{
String nAttachment = (String) attachments.get(k);
if (previousAttachments.indexOf(nAttachment) == -1)
{
previousAttachments = previousAttachments.concat(",").concat(nAttachment);
}
}
if (StringUtils.trimToNull(previousAttachments) != null)
{
propertiesMap.put(PROP_SUBMISSION_PREVIOUS_FEEDBACK_ATTACHMENTS, StringUtils.trimToNull(previousAttachments));
}
}
// reset the properties
keepItem.saxSerializableProperties.setSerializableProperties(propertiesMap);
}
// feedback attachments
keepItem.setFeedbackattachments(combineAttachments(keepItem.getFeedbackattachments(), removeItem.getFeedbackattachments()));
// submitted attachments
keepItem.setSubmittedattachments(combineAttachments(keepItem.getSubmittedattachments(), removeItem.getSubmittedattachments()));
if (removeItem.getDatesubmitted() != null)
{
//submission text
keepItem.setSubmittedtext(combineText(keepItem.getSubmittedtext(), removeItem.getSubmittedtext(), "submitted on " + removeItem.getDatesubmitted()));
//submission_text_html
keepItem.setSubmittedtext_html(combineText(keepItem.getSubmittedtext_html(), removeItem.getSubmittedtext_html(), "submitted on " + removeItem.getDatesubmitted()));
}
// feedback comment
keepItem.setFeedbackcomment(combineText(keepItem.getFeedbackcomment(), removeItem.getFeedbackcomment(), "commented on " + removeItem.getDatereturned()));
// feedback_comment_html
keepItem.setFeedbackcomment_html(combineText(keepItem.getFeedbackcomment_html(), removeItem.getFeedbackcomment_html(), "commented on " + removeItem.getDatereturned()));
// feedback text
keepItem.setFeedbacktext(combineText(keepItem.getFeedbacktext(), removeItem.getFeedbacktext(), "commented on " + removeItem.getDatereturned()));
// feedback_text_html
keepItem.setFeedbacktext_html(combineText(keepItem.getFeedbacktext_html(), removeItem.getFeedbacktext_html(), "commented on " + removeItem.getDatereturned()));
// review
if(keepItem.getReviewReport() == null && removeItem.getReviewReport() != null)
{
keepItem.setReviewReport(removeItem.getReviewReport());
}
if(keepItem.getReviewScore() == null && removeItem.getReviewScore() != null)
{
keepItem.setReviewScore(removeItem.getReviewScore());
}
if(keepItem.getReviewStatus() == null && removeItem.getReviewStatus() != null)
{
keepItem.setReviewStatus(removeItem.getReviewStatus());
}
// what to do with properties????
/// for now, we dump all the properties of the removeItem
// if(keepItem.getSerializableProperties() == null)
// {
// keepItem.setSerializableProperties(removeItem.getSerializableProperties());
// }
return keepItem;
}
/**
* is this grade with a non default value?
* @param grade
* @return
*/
private boolean nonDefaultGrade(String grade)
{
// if the grade is not of the following pattern, consider it is useful
boolean rv = false;
if (grade == null)
{
}
else if ("00".equals(grade))
{
}
else if ("0".equals(grade))
{
}
else if ("no grade".equals(grade))
{
}
else if ("ungraded".equals(grade))
{
}
else if ("Fail".equals(grade))
{
}
else
{
rv = true;
}
return rv;
}
/**
* Whether both grades
* @param item1
* @param item2
* @return
*/
private boolean bothGradesReleasedAndDifferent(AssignmentSubmissionAccess item1, AssignmentSubmissionAccess item2)
{
// if both grades have been released and are different
if (item1.getGradereleased().equalsIgnoreCase(Boolean.TRUE.toString()) && item2.getGradereleased().equalsIgnoreCase(Boolean.TRUE.toString()))
{
String grade1 = item1.getGrade();
String grade2 = item2.getGrade();
if (nonDefaultGrade(grade1) && nonDefaultGrade(grade2) && !grade1.equals(grade2))
{
// both grades are not of default grade, and also different
// need to keep the record
return true;
}
}
return false;
}
/**
* combine text, both are Base64 encoded
* @param text
* @param rText
* @param date
* @return
*/
private String combineText(String text, String rText, String date) {
return combine(text, true, rText, true, date);
}
/**
* combine plain text with Base64 encoded text
* @param text
* @param rText
* @param date
* @return
*/
private String combinePropertyWithText(String text, String rText, String date) {
return combine(text, false, rText, true, date);
}
/**
* combine grades, both are not encoded
* @param text
* @param rText
* @param date
* @return
*/
private String combineGrades(String text, String rText, String date) {
return combine(text, false, rText, false, date);
}
private String combine(String text, boolean textEncoded, String rText, boolean rTextEncoded, String date) {
text = StringUtils.trimToNull(text);
rText = StringUtils.trimToNull(rText);
if(rText != null && date != null)
{
String decodedRText = rText;
if (rTextEncoded)
{
try
{
decodedRText = new String(Base64.decodeBase64(rText.getBytes("UTF-8")));
}catch (java.io.UnsupportedEncodingException ignore)
{
// ignore
decodedRText = rText;
log.warn(this + ":combine " + ignore.getMessage());
}
}
if (text == null)
{
// use the rText instead
text = decodedRText;
}
else
{
try
{
String decodedText = text;
if (textEncoded)
{
try
{
decodedText = new String(Base64.decodeBase64(text.getBytes("UTF-8")));
}
catch (UnsupportedEncodingException e)
{
decodedText = text;
log.warn(this + ":combine " + e.getMessage());
}
}
if (decodedText.indexOf((decodedRText)) == -1)
{
String decoded= decodedText + "<p>" + date + ":</p><p>" + decodedRText + "</p>";
if (textEncoded)
{
// return encoded
try
{
text = new String(Base64.encodeBase64(decoded.getBytes()),"UTF-8");
}
catch (java.io.UnsupportedEncodingException e)
{
// ignore
log.warn(this + ":combine 2 " + e.getMessage());
}
}
else
{
// return plain
text = decoded;
}
}
}catch (Exception ee)
{
// ignore
log.warn(" Combine: " + ee.getMessage());
}
}
}
return text;
}
/**
* coombine attachments
* @param attachments
* @param rAttachments
* @return
*/
private List<String> combineAttachments(List<String> attachments, List<String> rAttachments) {
if(rAttachments != null && !rAttachments.isEmpty())
{
if (attachments == null || attachments.isEmpty())
{
// if keepItem's attachment is empty, use the removeItem's instead
attachments = rAttachments;
}
else
{
// find the missing attachment from removeItem, and add them to keepItem
for(int i=0; i<rAttachments.size();i++)
{
String rAttachment = rAttachments.get(i);
if (!attachments.contains(rAttachment))
{
// add this attachment from removeItem
attachments.add(rAttachment);
}
}
}
}
return attachments;
}
public Object getSource(String id, ResultSet rs) throws SQLException
{
List<String> xml = new ArrayList<String>();
while (rs.next())
{
xml.add(rs.getString(1));
}
return xml;
}
public Object getValidateSource(String id, ResultSet rs)
throws SQLException
{
// TODO Auto-generated method stub
return null;
}
public void validate(String id, Object source, Object result)
throws Exception
{
}
/**
* get Integer based on passed string. Truncate the String if necessary
* @param timeString
* @return
*/
private Integer getIntegerObject(String timeString)
{
Integer rv = null;
int max_length = Integer.valueOf(Integer.MAX_VALUE).toString().length();
if (timeString.length() > max_length)
{
timeString = timeString.substring(0, max_length);
}
try
{
rv = Integer.parseInt(timeString);
}
catch (Exception e)
{
// ignore
log.warn(this + ":getIntegerObject " + e.getMessage());
}
return rv;
}
}