package ca.carleton.gcrc.couch.submission.impl;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ca.carleton.gcrc.couch.client.CouchDb;
import ca.carleton.gcrc.couch.client.CouchDesignDocument;
import ca.carleton.gcrc.couch.client.CouchDocumentOptions;
import ca.carleton.gcrc.couch.client.CouchQuery;
import ca.carleton.gcrc.couch.client.CouchQueryResults;
import ca.carleton.gcrc.couch.client.CouchUserDb;
import ca.carleton.gcrc.couch.client.CouchUserDocContext;
import ca.carleton.gcrc.couch.submission.SubmissionRobotSettings;
import ca.carleton.gcrc.couch.submission.mail.SubmissionMailNotifier;
import ca.carleton.gcrc.json.JSONSupport;
import ca.carleton.gcrc.json.patcher.JSONPatcher;
import ca.carleton.gcrc.mail.MailRecipient;
public class SubmissionRobotThread extends Thread {
final protected Logger logger = LoggerFactory.getLogger(this.getClass());
private boolean isShuttingDown = false;
private CouchDesignDocument submissionDbDesignDocument;
private CouchDesignDocument documentDbDesignDocument;
private CouchUserDb userDb;
private SubmissionMailNotifier mailNotifier = null;
private Set<String> docIdsToSkip = new HashSet<String>();
private String adminRole = "administrator";
private String vetterRole = "vetter";
public SubmissionRobotThread(SubmissionRobotSettings settings) {
this.submissionDbDesignDocument = settings.getSubmissionDesignDocument();
this.documentDbDesignDocument = settings.getDocumentDesignDocument();
this.userDb = settings.getUserDb();
this.mailNotifier = settings.getMailNotifier();
if( null != settings.getAtlasName() ){
adminRole = settings.getAtlasName() + "_administrator";
vetterRole = settings.getAtlasName() + "_vetter";
}
}
public void shutdown() {
logger.info("Shutting down submission worker thread");
synchronized(this) {
isShuttingDown = true;
this.notifyAll();
}
}
@Override
public void run() {
logger.info("Start submission worker thread");
boolean done = false;
do {
synchronized(this) {
done = isShuttingDown;
}
if( false == done ) {
activity();
}
} while( false == done );
logger.info("Submission worker thread exiting");
}
private void activity() {
CouchQuery query = new CouchQuery();
query.setViewName("submission-work");
CouchQueryResults results;
try {
results = submissionDbDesignDocument.performQuery(query);
} catch (Exception e) {
logger.error("Error accessing submission database",e);
waitMillis(60 * 1000); // wait a minute
return;
}
// Check for work
String docId = null;
for(JSONObject row : results.getRows()) {
String id = row.optString("id");
if( false == docIdsToSkip.contains(id) ) {
// Found some work
docId = id;
break;
}
}
if( null == docId ) {
// Nothing to do, wait 4 secs
waitMillis(4 * 1000);
return;
} else {
try {
// Handle this work
performWork(docId);
} catch(Exception e) {
logger.error("Error processing document "+docId,e);
docIdsToSkip.add(docId);
}
}
}
/*
* submitted (robot)
* -> complete (if target document is deleted)
* -> approved (if submitted by someone who is automatically approved)
* -> waiting_for_approval (otherwise)
*
* approved (robot)
* -> complete (if target document is deleted)
* -> complete (if target document can be updated automatically)
* -> collision (if merging to target document performs a collision)
*
* waiting_for_approval (user)
* -> approved (when an administrator agrees with changes)
* -> denied (when an administrator disagrees with changes)
*
* collision (user)
* -> resolved (when an administrator fixes the changes to avoid collision)
* -> denied (when administrator decides changes are no longer wanted)
*
* resolved (robot)
* -> complete (if target document is deleted)
* -> complete (if changes are merged on target document)
* -> collision (if changes can not be merged on target document)
*
* denied (user)
* -> approved (when administrator decides that changes are needed)
*
* complete
*/
public void performWork(String submissionDocId) throws Exception {
// Get submission document
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
JSONObject submissionDoc = submissionDb.getDocument(submissionDocId);
// Get document id
String docId = submissionDoc
.getJSONObject("nunaliit_submission")
.getJSONObject("original_reserved")
.getString("id");
String revision = submissionDoc
.getJSONObject("nunaliit_submission")
.getJSONObject("original_reserved")
.optString("rev",null);
// Check if denial email must be sent
boolean sendDenialEmail = false;
JSONObject denialEmail = submissionDoc
.getJSONObject("nunaliit_submission")
.optJSONObject("denial_email");
if( null != denialEmail ){
boolean requested = denialEmail.optBoolean("requested",false);
boolean sent = denialEmail.optBoolean("sent",false);
if( requested && !sent ){
sendDenialEmail = true;
}
}
// Get document in document database
CouchDb documentDb = documentDbDesignDocument.getDatabase();
JSONObject doc = null;
try {
doc = documentDb.getDocument(docId);
} catch(Exception e) {
// ignore
}
if( null == doc
&& null != revision ) {
// Referenced document no longer exists
submissionDoc.getJSONObject("nunaliit_submission")
.put("state", "complete");
submissionDb.updateDocument(submissionDoc);
} else {
String stateStr = null;
JSONObject jsonSubmission = submissionDoc.getJSONObject("nunaliit_submission");
stateStr = jsonSubmission.optString("state",null);
if( null == stateStr ) {
performSubmittedWork(submissionDoc, doc);
} else if( "submitted".equals(stateStr) ) {
performSubmittedWork(submissionDoc, doc);
} else if( "approved".equals(stateStr) ) {
performApprovedWork(submissionDoc, doc);
} else if( sendDenialEmail ) {
performDenialEmail(submissionDoc, doc);
} else {
throw new Exception("Unexpected state for submission document: "+stateStr);
}
}
}
public void performSubmittedWork(JSONObject submissionDoc, JSONObject targetDoc) throws Exception {
// Find roles associated with the user who submitted the change
String userId = submissionDoc
.getJSONObject("nunaliit_last_updated")
.getString("name");
CouchUserDocContext userDoc = null;
try {
userDoc = userDb.getUserFromName(userId);
} catch(Exception e) {
// Ignore if we can not find user
}
// Check if submission should be automatically approved
boolean approved = false;
if( null != userDoc ) {
List<String> roles = userDoc.getRoles();
for(String role : roles){
if( "_admin".equals(role) ){
approved = true;
break;
} else if( "administrator".equals(role) ){
approved = true;
break;
} else if( "vetter".equals(role) ){
approved = true;
break;
} else if( adminRole.equals(role) ){
approved = true;
break;
} else if( vetterRole.equals(role) ){
approved = true;
break;
}
}
}
if( approved ) {
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
submissionDoc.getJSONObject("nunaliit_submission")
.put("state", "approved");
submissionDb.updateDocument(submissionDoc);
} else {
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
submissionDoc.getJSONObject("nunaliit_submission")
.put("state", "waiting_for_approval");
submissionDb.updateDocument(submissionDoc);
logger.error("Sending waiting for approval notification for submission");
this.mailNotifier.sendSubmissionWaitingForApprovalNotification(submissionDoc);
}
}
public void performApprovedWork(JSONObject submissionDoc, JSONObject currentDoc) throws Exception {
String docId = submissionDoc
.getJSONObject("nunaliit_submission")
.getJSONObject("original_reserved")
.getString("id");
boolean isDeletion = submissionDoc
.getJSONObject("nunaliit_submission")
.optBoolean("deletion",false);
if( null == currentDoc ) {
// New document. Create.
JSONObject originalDoc = SubmissionUtils.getApprovedDocumentFromSubmission(submissionDoc);
CouchDb targetDb = documentDbDesignDocument.getDatabase();
targetDb.createDocument(originalDoc);
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
submissionDoc.getJSONObject("nunaliit_submission")
.put("state", "complete");
submissionDb.updateDocument(submissionDoc);
} else if( isDeletion ) {
CouchDb targetDb = documentDbDesignDocument.getDatabase();
JSONObject toDeleteDoc = targetDb.getDocument(docId);
targetDb.deleteDocument(toDeleteDoc);
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
submissionDoc.getJSONObject("nunaliit_submission")
.put("state", "complete");
submissionDb.updateDocument(submissionDoc);
} else {
String currentVersion = currentDoc.getString("_rev");
JSONObject approvedDoc = SubmissionUtils.getApprovedDocumentFromSubmission(submissionDoc);
String approvedVersion = approvedDoc.optString("_rev",null);
if( currentVersion.equals(approvedVersion) ) {
// No changes since approval. Simply update the document
// database.
CouchDb targetDb = documentDbDesignDocument.getDatabase();
targetDb.updateDocument(approvedDoc);
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
submissionDoc.getJSONObject("nunaliit_submission")
.put("state", "complete");
submissionDb.updateDocument(submissionDoc);
} else {
// Get document that the changes were made against
CouchDb couchDb = documentDbDesignDocument.getDatabase();
CouchDocumentOptions options = new CouchDocumentOptions();
options.setRevision(approvedVersion);
JSONObject rootDoc = couchDb.getDocument(docId, options);
// Compute patch from submission
JSONObject submissionPatch = JSONPatcher.computePatch(rootDoc, approvedDoc);
JSONObject databasePatch = JSONPatcher.computePatch(rootDoc, currentDoc);
// Detect collision. Apply patches in different order, if result
// is same, then no collision
JSONObject doc1 = JSONSupport.copyObject(rootDoc);
JSONPatcher.applyPatch(doc1, submissionPatch);
JSONPatcher.applyPatch(doc1, databasePatch);
JSONObject doc2 = JSONSupport.copyObject(rootDoc);
JSONPatcher.applyPatch(doc2, databasePatch);
JSONPatcher.applyPatch(doc2, submissionPatch);
if( 0 == JSONSupport.compare(doc1, doc2) ) {
// No collision
logger.error("rootDoc: "+rootDoc);
logger.error("submissionPatch: "+submissionPatch);
logger.error("databasePatch: "+databasePatch);
logger.error("no collision: "+doc1);
CouchDb targetDb = documentDbDesignDocument.getDatabase();
targetDb.updateDocument(doc1);
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
submissionDoc.getJSONObject("nunaliit_submission")
.put("state", "complete");
submissionDb.updateDocument(submissionDoc);
} else {
// Collision case
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
submissionDoc.getJSONObject("nunaliit_submission")
.put("state", "collision");
submissionDb.updateDocument(submissionDoc);
}
}
}
}
public void performDenialEmail(JSONObject submissionDoc, JSONObject currentDoc) throws Exception {
JSONObject submissionInfo = submissionDoc.getJSONObject("nunaliit_submission");
JSONObject denial_email = submissionInfo.getJSONObject("denial_email");
// Find user that submitted the update
String userId = null;
JSONObject created = submissionDoc.optJSONObject("nunaliit_created");
if( null != created ){
userId = created.optString("name", null);
}
// Get user document
CouchUserDocContext userDocContext = null;
if( null != userId ){
try {
userDocContext = userDb.getUserFromName(userId);
} catch(Exception e) {
// Ignore if we can not find user
}
}
// Get list of e-mails
List<String> emails = new Vector<String>();
String userName = null;
if( null != userDocContext ){
JSONObject userDoc = userDocContext.getUserDoc();
Set<String> validatedEmails = new HashSet<String>();
JSONArray jsonValidated = userDoc.optJSONArray("nunaliit_validated_emails");
if( null != jsonValidated ){
for(int i=0; i<jsonValidated.length(); ++i){
String email = jsonValidated.getString(i);
validatedEmails.add(email);
}
}
JSONArray jsonEmails = userDoc.optJSONArray("nunaliit_emails");
if( null != jsonEmails ){
for(int i=0; i<jsonEmails.length(); ++i){
String email = jsonEmails.getString(i);
if( validatedEmails.contains(email) ){
emails.add(email);
}
}
}
userName = userDoc.optString("display",null);
if( null == userName ){
userName = userDoc.optString("name",null);
}
}
// If no e-mails, just quit
if( emails.size() < 1 ){
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
denial_email.put("sent", true);
submissionDb.updateDocument(submissionDoc);
return;
}
// Convert e-mail addresses into recipient
List<MailRecipient> recipients = new ArrayList<MailRecipient>(emails.size());
for(String email : emails){
MailRecipient recipient = null;
if( null != userName ){
recipient = new MailRecipient(email, userName);
} else {
recipient = new MailRecipient(email);
}
recipients.add(recipient);
}
// Send notification
mailNotifier.sendSubmissionRejectionNotification(submissionDoc, recipients);
// Remember it was sent
CouchDb submissionDb = submissionDbDesignDocument.getDatabase();
denial_email.put("sent", true);
submissionDb.updateDocument(submissionDoc);
}
private boolean waitMillis(int millis) {
synchronized(this) {
if( true == isShuttingDown ) {
return false;
}
try {
this.wait(millis);
} catch (InterruptedException e) {
// Interrupted
return false;
}
}
return true;
}
}