package org.sana.android.procedure;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.sana.R;
import org.sana.android.Constants;
import org.sana.android.db.PatientInfo;
import org.sana.android.db.PatientValidator;
import org.sana.android.util.EnvironmentUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.ViewAnimator;
/**
* A Procedure is, conceptually, a form that can be made up of a number of
* pages, each of which may contain several elements. Since pages may contain
* entry criteria (checks that allow the procedure to branch if previous
* responses were made a certain way), the methods in the Procedure take care of
* checking these criteria.
*
* @author Sana Development Team
*/
public class Procedure {
public static final String TAG = Procedure.class.getSimpleName();
private View cachedView;
private Context cachedContext;
private String onComplete = null;
private Uri instanceUri = null;
private String title;
private String author;
private String guid;
private String concept = null;
private List<ProcedurePage> pages;
public ListIterator<ProcedurePage> pagesIterator;
private ProcedurePage currentPage;
private ViewAnimator viewAnimator;
private PatientInfo patientInfo = null;
private String version = "1.0";
private boolean showQuestionIds = false;
/**
* Constructs a new Procedure.
*
* @param title A title string.
* @param author The author.
* @param guid A unique identifier within the context of this application
* instance.
* @param pages A list of pages contained within this procedure.
* @param elements A map of all of the Procedure elements referenced by this
* procedure.
*/
public Procedure(String title, String author, String guid,
List<ProcedurePage> pages, HashMap<String, ProcedureElement> elements)
{
this.pages = new LinkedList<ProcedurePage>();
//this.pages.addAll(pages);
for(ProcedurePage pp : pages) {
pp.setProcedure(this);
this.pages.add(pp);
}
this.title = title;
this.author = author;
this.guid = guid;
pagesIterator = pages.listIterator();
next();
}
/**
* Constructor which provides the onComplete String
* @param title
* @param author
* @param guid
* @param pages
* @param elements
* @param onComplete
*/
public Procedure(String title, String author, String guid, List<ProcedurePage> pages, HashMap<String,
ProcedureElement> elements, String onComplete)
{
this(title,author,guid,pages,elements);
this.onComplete = onComplete;
}
public void init() {
}
/**
* Sets the view referenced by this procedure
*
* @param v the new View
*/
public void setCachedView(View v){
this.cachedView = v;
}
/**
* Gets the cached view.
* @return a View instance.
*/
public View getCachedView(){
return this.cachedView;
}
/**
* Sets the Uri of this instance in the database
*
* @param instanceUri the new instance Uri
*/
public void setInstanceUri(Uri instanceUri) {
this.instanceUri = instanceUri;
}
/**
* Gets the instance Uri for this procedure.
* @return A resource identifier for this procedure
*/
public Uri getInstanceUri() {
return instanceUri;
}
/**
* The current, or active, page.
* @return
*/
public ProcedurePage current() {
return currentPage;
}
/**
* Sets the data for the patient referenced in this procedure.
* @param pi the new patient data
*/
public void setPatientInfo(PatientInfo pi) {
this.patientInfo = pi;
}
/**
* Gets the data for the patient referenced by this procedure
* @return The patient data.
*/
public PatientInfo getPatientInfo() {
return patientInfo;
}
/**
* Determines whether there is a next page in the sequence. It does
* <b>NOT</b> check whether that page should be viewed or not.
*
* @return true if there is at least one page past the current.
*/
public boolean hasNext() {
if(pagesIterator == null)
return false;
return pagesIterator.hasNext();
}
/**
* Determines whether there is a previous page in the sequence. It does
* <b>NOT</b> check whether that page should be viewed or not.
*
* @return true if there is at least one page before the current.
*/
public boolean hasPrev() {
if(pagesIterator == null)
return false;
if(pagesIterator.previousIndex() == 0) {
return false;
}
return true;
}
/**
* Advances to the next page in the sequence. It does <b>NOT</b> check
* whether that page should be viewed or not given user choices.
*/
public void next() {
if (hasNext()) {
currentPage = pagesIterator.next();
if(viewAnimator != null && cachedContext != null) {
viewAnimator.setInAnimation(cachedContext,R.anim.slide_from_right);
viewAnimator.setOutAnimation(cachedContext,R.anim.slide_to_left);
viewAnimator.showNext();
}
}
}
/**
* Goes back to the previous page in the sequence. It does <b>NOT</b> check
* whether that page should be viewed or not given user choices.
*/
public void prev() {
if (hasPrev()) {
currentPage = pagesIterator.previous();
if(viewAnimator != null && cachedContext != null) {
viewAnimator.setInAnimation(cachedContext,R.anim.slide_from_left);
viewAnimator.setOutAnimation(cachedContext,R.anim.slide_to_right);
viewAnimator.showPrevious();
}
}
}
/**
* Determines whether there is a next show-able page in the sequence, given
* user selections thus far.
*
* @return true if there is one page past the current and that page is
* visible
*/
public boolean hasNextShowable() {
if(pagesIterator == null)
return false;
if (!pagesIterator.hasNext())
return false;
for (int i = pagesIterator.nextIndex(); i < pages.size(); i++) {
if (pages.get(i).shouldDisplay())
return true;
}
return false;
}
/**
* Determines whether there is a previous show-able page in the sequence,
* given user selections thus far.
*
* @return true if there is one page before the current and that page is
* visible
*/
public boolean hasPrevShowable() {
if (pagesIterator == null)
return false;
if (!pagesIterator.hasPrevious())
return false;
if (pagesIterator.previousIndex() == 0)
return false;
for (int i = pagesIterator.previousIndex(); i >= 0; i--) {
if (pages.get(i).shouldDisplay())
return true;
}
return false;
}
/**
* Advances the current page to the next show-able page in the sequence,
* skipping over non-show-able pages, given user selections thus far. It
* also updates the procedure view to advance by this same number of pages.
*/
public void advance() {
if (!hasNextShowable())
return;
ProcedurePage pp = pagesIterator.next();
viewAnimator.showNext();
while (hasNext() && !pp.shouldDisplay()) {
pp = pagesIterator.next();
viewAnimator.showNext();
}
currentPage = pp;
// Fill in default values for data from patient in the database
PatientValidator.populateSpecialElements(this, patientInfo);
}
public ProcedurePage advanceNext() {
if (!hasNext())
return null;
currentPage = pagesIterator.next();
viewAnimator.showNext();
// Fill in default values for data from patient in the database
PatientValidator.populateSpecialElements(this, patientInfo);
return currentPage;
}
public ProcedurePage advancePrev() {
if (!hasPrev())
return null;
currentPage = pagesIterator.previous();
viewAnimator.showPrevious();
// Fill in default values for data from patient in the database
PatientValidator.populateSpecialElements(this, patientInfo);
return currentPage;
}
/**
* Regresses the current page to the previous show-able page in the
* sequence, skipping over non-show-able pages, given user selections thus
* far. It also updates the procedure view to regress by this same number of
* pages.
*/
public void back() {
if (!hasPrevShowable())
return;
ProcedurePage pp;
// this will refer to the current page
pagesIterator.previous();
pp = pages.get(pagesIterator.previousIndex());
viewAnimator.showPrevious();
while (hasPrev() && !pp.displayForeground()) {
pagesIterator.previous();
pp = pages.get(pagesIterator.previousIndex());
viewAnimator.showPrevious();
}
currentPage = pp;
}
/**
* Sets the current page index to a specified value as an offset from zero.
*
* @param pageIndex The index of the page to jump to.
*/
public void jumpToPage(int pageIndex) {
if (pageIndex < 0 || pageIndex >= pages.size()) {
return;
}
pagesIterator = pages.listIterator();
Log.i(TAG, "pageIndex value: " + pageIndex);
while(pagesIterator.nextIndex() != pageIndex) {
pagesIterator.next();
}
Log.i(TAG, "current index of page: " + getCurrentIndex());
currentPage = pagesIterator.next();
Log.i(TAG, "current index of page: " + getCurrentIndex());
viewAnimator.setInAnimation(null);
viewAnimator.setOutAnimation(null);
viewAnimator.setDisplayedChild(pageIndex);
}
/**
* Sets the current page index to a specified value as an offset from zero
* if and only if that page is viewable. If not viewable, the current index
* is unchanged
*
* @param pageIndex The index of the page to jump to.
*/
public void jumpToVisiblePage(int pageIndex) {
if (pageIndex < 0 || pageIndex >= pages.size())
return;
pagesIterator = pages.listIterator();
int visibleIndex = 0;
int actualIndex = 0;
while (pagesIterator.hasNext()) {
ProcedurePage page = pagesIterator.next();
if (visibleIndex == pageIndex) {
currentPage = page;
viewAnimator.setInAnimation(null);
viewAnimator.setOutAnimation(null);
viewAnimator.setDisplayedChild(actualIndex);
break;
}
if (page.shouldDisplay()) {
visibleIndex++;
}
actualIndex++;
}
}
/**
* Gets the index value of the current page.
*
* @return The index value of the current page
*/
public int getCurrentIndex() {
return pages.indexOf(currentPage);
}
/**
* Gets the index value of the current page if visible.
*
* @return The index value of the current page if visible else 0.
*/
public int getCurrentVisibleIndex() {
Iterator<ProcedurePage> pageIterator = pages.iterator();
int visibleIndex = 0;
while (pageIterator.hasNext()) {
ProcedurePage page = pageIterator.next();
if (page == currentPage) {
return visibleIndex;
}
if (page.shouldDisplay()) {
visibleIndex++;
}
}
return 0;
}
/**
* The mu,ber of pages in this procedure.
* @return The total number of pages
*/
public int getTotalPageCount() {
return pages.size();
}
/**
* The number of viewable pages in this procedure.
* @return The total number of pages
*/
public int getVisiblePageCount() {
Iterator<ProcedurePage> pageIterator = pages.iterator();
int visibleCount = 0;
while (pageIterator.hasNext()) {
ProcedurePage page = pageIterator.next();
if (page.shouldDisplay()) {
visibleCount++;
}
}
return visibleCount;
}
/**
* The procedure title.
* @return The procedure title string.
*/
public String getTitle() {
return title;
}
/**
* The procedure author
* @return The procedure author string .
*/
public String getAuthor() {
return author;
}
/**
* Gets the unique identifier.
* @return The guid.
*/
public String getGuid() {
return guid;
}
public String getVersion(){
return version;
}
public void setVersion(String version){
this.version = version;
}
public String getOnComplete() {
return onComplete;
}
public void setOnComplete(String onComplete) {
this.onComplete = onComplete;
}
public String getConcept(){ return concept; }
public void setConcept(String concept){ this.concept=concept; }
public boolean idsShown(){
return showQuestionIds;
}
public void setShowQuestionIds(boolean value){
this.showQuestionIds = value;
}
/**
* Writes the procedure, including all of its child elements, to an XML
* String.
* @return The procedure as xml text.
*/
public String toXML() {
Log.i(TAG,"Procedure.toXML()");
StringBuilder sb = new StringBuilder();
buildXML(sb);
return sb.toString();
}
// ugly way to do this
//TODO use a proper data dictionary for obs value
public void setValue(String elementId, String value){
for(ProcedurePage page:pages){
page.setElementValue(elementId, value);
}
}
// a bit better but still hacky
public boolean setValue(int pageIndex, String elementId, String value){
boolean result = false;
if(pageIndex > -1 && pageIndex < pages.size()){
pages.get(pageIndex).setElementValue(elementId, value);
result = true;
} else {
Log.w(TAG, "setValue(). Index Out of bounds. " + pageIndex );
}
return result;
}
/**
* Writes the xml for this procedure to a StringBuilder object.
* @param sb The builder to write to.
*/
public void buildXML(StringBuilder sb) {
sb.append("<Procedure title =\"" + title
+ "\" author =\"" + author
+ "\" guid =\"" + guid
+ "\" version=\"" + version
+ "\" uuid=\"" + guid
+ "\" concept=\"" + guid
+ "\" onComplete=\"" + guid
+ "\">\n");
for (ProcedurePage p : pages) {
p.buildXML(sb);
}
sb.append("</Procedure>");
}
/**
* A map of the 'id' to 'answer' attributes for all of the data collection
* points in this procedure.
*
* @return The answers for all of the ProcedureElements mapped to their ids
*/
public Map<String, String> toAnswers() {
HashMap<String,String> answers = new HashMap<String,String>();
for(ProcedurePage pp : pages) {
pp.populateAnswers(answers);
}
return answers;
}
/**
* Takes a map of answers and fills them into the elements of this
* procedure. Functionally, this is used to return a prior patient encounter
* to the state it was in when saved
* @param answersMap The answers for all of the ProcedureElements mapped to
* their ids
*/
public void restoreAnswers(Map<String,String> answersMap) {
for (ProcedurePage pp : pages) {
pp.restoreAnswers(answersMap);
}
}
/**
* Produces a new map of element properties to their ids.
*
* @return a dictionary mapping Element ids to a dictionary containing the
* properties for each Element
*/
public Map<String, Map<String,String>> toElementMap() {
HashMap<String,Map<String,String>> answers =
new HashMap<String,Map<String,String>>();
for(ProcedurePage pp : pages) {
pp.populateElementMap(answers);
}
return answers;
}
// Constructs a Procedure object from xml text
private static Procedure fromXML(Node node) throws ProcedureParseException {
if(!node.getNodeName().equals("Procedure")) {
throw new ProcedureParseException("Procedure got NodeName"
+ node.getNodeName());
}
List<ProcedurePage> pages = new ArrayList<ProcedurePage>();
NodeList nl = node.getChildNodes();
ProcedurePage page;
HashMap<String, ProcedureElement> elts =
new HashMap<String, ProcedureElement>();
for(int i=0; i<nl.getLength(); i++) {
Node child = nl.item(i);
if(child.getNodeName().equals("Page")) {
page = ProcedurePage.fromXML(child, elts);
elts.putAll(page.getElementMap());
pages.add(page);
}
}
String title = "Untitled Procedure";
Node titleNode = node.getAttributes().getNamedItem("title");
if(titleNode != null) {
title = titleNode.getNodeValue();
Log.i(TAG, "Loading Procedure from XML: " + title);
}
String author = "";
Node authorNode = node.getAttributes().getNamedItem("author");
if(authorNode != null) {
author = authorNode.getNodeValue();
Log.i(TAG, "Author of this procedure: " + author);
}
String uuid = "";
Node guidNode = node.getAttributes().getNamedItem("uuid");
if(guidNode != null) {
uuid = guidNode.getNodeValue();
Log.i(TAG, "Unique Id of procedure: " + uuid);
}
String version = "";
Node n = node.getAttributes().getNamedItem("version");
if(n != null) {
version = n.getNodeValue();
Log.i(TAG, "Version: " + version);
}
String onComplete = "";
n = node.getAttributes().getNamedItem("on_complete");
if(n != null) {
onComplete = n.getNodeValue();
Log.i(TAG, "Version: " + version);
}
String concept = "";
n = node.getAttributes().getNamedItem("concept");
if(n != null) {
concept = n.getNodeValue();
Log.i(TAG, "Concept: " + version);
}
Procedure procedure = new Procedure(title, author, uuid, pages, elts);
procedure.setVersion(version);
procedure.setConcept(concept);
procedure.setOnComplete(onComplete);
return procedure;
}
/**
* Constructs a new Procedure from a raw xml resource.
* @param c the current Context.
* @param id The resource identifier
* @return A new Procedure instance.
* @throws IOException
* @throws ParserConfigurationException
* @throws SAXException
* @throws Exception
*/
public static Procedure fromRawResource(Context c, int id) throws
IOException, ParserConfigurationException, SAXException, Exception
{
InputStream is = null;
is = c.getResources().openRawResource(id);
Procedure p = fromXML(new InputSource(is));
return p;
}
/**
* Constructs a new Procedure from an xml string.
* @param xml The xml string to read.
* @return A new Procedure instance.
* @throws IOException
* @throws ParserConfigurationException
* @throws SAXException
* @throws Exception
*/
public static Procedure fromXMLString(String xml) throws IOException,
ParserConfigurationException, SAXException, ProcedureParseException
{
return fromXML(new InputSource(new StringReader(xml)));
}
/**
* Constructs a new Procedure from an InputSource.
* @param xml The InputSource to read.
* @return A new Procedure instance.
* @throws IOException
* @throws ParserConfigurationException
* @throws SAXException
* @throws Exception
*/
public static Procedure fromXML(InputSource xml) throws IOException,
ParserConfigurationException, SAXException, ProcedureParseException
{
long processingTime = System.currentTimeMillis();
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setValidating(false);
dbf.setIgnoringComments(true);
dbf.setIgnoringElementContentWhitespace(true);
dbf.setNamespaceAware(false);
DocumentBuilder db = dbf.newDocumentBuilder();
Document d = db.parse(xml);
NodeList children = d.getChildNodes();
Node procedureNode = null;
for(int i=0; i<children.getLength(); i++) {
Node child = d.getChildNodes().item(i);
if(child.getNodeName().equals("Procedure")) {
procedureNode = child;
break;
}
}
if(procedureNode == null) {
throw new ProcedureParseException("Can't get procedure");
}
Procedure result = fromXML(procedureNode);
processingTime = System.currentTimeMillis() - processingTime;
Log.i(TAG, "Parsing procedure XML took " + processingTime + " milliseconds.");
return result;
}
// creates the views for this object and indirectly all of its child pages
private View createView(Context c) {
viewAnimator = new ViewAnimator(c);
//viewAnimator.setInAnimation(AnimationUtils.loadAnimation(c,R.anim.slide_from_right));
//viewAnimator.setOutAnimation(AnimationUtils.loadAnimation(c,R.anim.slide_to_left));
for(ProcedurePage page : pages) {
viewAnimator.addView(page.toView(c));
}
return viewAnimator;
}
/**
* Clears any views cached in this object.
*/
public void clearCachedViews() {
cachedView = null;
cachedContext = null;
for (ProcedurePage pp : pages) {
pp.clearCachedView();
}
}
/**
* A View of this object.
* @param c the current Context.
* @return An existing view of this object if cached or a new one.
*/
public View toView(Context c) {
Log.i(TAG,"toView(Context)");
if(cachedView == null || cachedContext != c) {
Log.d(TAG, "...generating cached view");
cachedView = createView(c);
cachedContext = c;
}
return cachedView;
}
/**
* A list of the summary strings for all child pages.
* @return The child pages as a list of summary strings.
*/
public ArrayList<String> toStringArray() {
ArrayList<String> stringList= new ArrayList<String>();
for (ProcedurePage cp : pages) {
if(cp.shouldDisplay()) {
stringList.add(cp.getSummary());
}
}
return stringList;
}
/**
* Creates the necessary directories and files on the external drive for
* procedure management.
*/
public static void intializeDevice(){
String mount = Environment.getExternalStorageState();
Log.d(TAG, "Media stat:" + mount);
if(!mount.equals(Environment.MEDIA_MOUNTED)){
Log.e(TAG, "Can not initialize sdcard procedure resource dir.");
return;
}
File p = new File(EnvironmentUtil.getProcedureDirectory());
File r = new File(Environment.getExternalStorageDirectory()
+ Constants.PATH_EDUCATION);
if (p.mkdirs() && r.mkdirs()){
Log.d(TAG, "Created Sana procedure directories");
} else {
Log.d(TAG, "Sana procedure directory failed. ");
}
}
}