# Source: js/models/PackageModel.js

    /*global define */
    define(['jquery', 'underscore', 'backbone', 'uuid', 'md5', 'rdflib', 'models/SolrResult', 'models/LogsSearch'],
    	function($, _, Backbone, uuid, md5, rdf, SolrResult, LogsSearch) {

    	// Package Model
    	// ------------------
    	var PackageModel = Backbone.Model.extend(
        /** @lends PackageModel.prototype */{
    		// This model contains information about a package/resource map
    		defaults: function(){
    			return {
    				id: null, //The id of the resource map/package itself
    				url: null, //the URL to retrieve this package
    				memberId: null, //An id of a member of the data package
    				indexDoc: null, //A SolrResult object representation of the resource map
    				size: 0, //The number of items aggregated in this package
    				totalSize: null,
    				formattedSize: "",
    				formatId: null,
    				obsoletedBy: null,
    				obsoletes: null,
    				read_count_i: null,
    				isPublic: true,
    				members: [],
    				memberIds: [],
    				sources: [],
    				derivations: [],
    				provenanceFlag: null,
    				sourcePackages: [],
    				derivationPackages: [],
    				sourceDocs: [],
    				derivationDocs: [],
    				relatedModels: [], //A condensed list of all SolrResult models related to this package in some way
    				parentPackageMetadata: null,
    				//If true, when the member objects are retrieved, archived content will be included
    				getArchivedMembers: false,
    			}
    		},

    		//Define the namespaces
            namespaces: {
    			RDF:     "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
    			FOAF:    "http://xmlns.com/foaf/0.1/",
    			OWL:     "http://www.w3.org/2002/07/owl#",
    			DC:      "http://purl.org/dc/elements/1.1/",
    			ORE:     "http://www.openarchives.org/ore/terms/",
    			DCTERMS: "http://purl.org/dc/terms/",
    			CITO:    "http://purl.org/spar/cito/",
    			XML:     "http://www.w3.org/2001/XMLSchema#"
    		},

    		sysMetaNodeMap: {
    			accesspolicy: "accessPolicy",
    			accessrule: "accessRule",
    			authoritativemembernode: "authoritativeMemberNode",
    			dateuploaded: "dateUploaded",
    			datesysmetadatamodified: "dateSysMetadataModified",
    			dateuploaded: "dateUploaded",
    			formatid: "formatId",
    			nodereference: "nodeReference",
    			obsoletedby: "obsoletedBy",
    			originmembernode: "originMemberNode",
    			replicamembernode: "replicaMemberNode",
    			replicapolicy: "replicaPolicy",
    			replicationstatus: "replicationStatus",
    			replicaverified: "replicaVerified",
    			rightsholder: "rightsHolder",
    			serialversion: "serialVersion"
    		},

    		complete: false,

    		pending: false,

    		type: "Package",

    		// The RDF graph representing this data package
            dataPackageGraph: null,

    		initialize: function(options){
    			this.setURL();

    			// Create an initial RDF graph
                this.dataPackageGraph = rdf.graph();
    		},

    		setURL: function(){
    			if(MetacatUI.appModel.get("packageServiceUrl"))
    				this.set("url", MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(this.get("id")));
    		},

    		/*
    		 * Set the URL for fetch
    		 */
    		url: function(){
    			return MetacatUI.appModel.get("objectServiceUrl") + encodeURIComponent(this.get("id"));
    		},

    		/* Retrieve the id of the resource map/package that this id belongs to */
    		getMembersByMemberID: function(id){
    			this.pending = true;

    			if((typeof id === "undefined") || !id) var id = this.memberId;

    			var model = this;

    			//Get the id of the resource map for this member
    			var provFlList = MetacatUI.appSearchModel.getProvFlList() + "prov_instanceOfClass,";
    			var query = 'fl=resourceMap,fileName,read:read_count_i,obsoletedBy,size,formatType,formatId,id,datasource,title,origin,pubDate,dateUploaded,isPublic,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription,' + provFlList +
    						'&rows=1' +
    						'&q=id:%22' + encodeURIComponent(id) + '%22' +
    						'&wt=json';

    			var requestSettings = {
    				url: MetacatUI.appModel.get("queryServiceUrl") + query,
    				success: function(data, textStatus, xhr) {
    					//There should be only one response since we searched by id
    					if(typeof data.response.docs !== "undefined"){
    						var doc = data.response.docs[0];

    						//Is this document a resource map itself?
    						if(doc.formatId == "http://www.openarchives.org/ore/terms"){
    							model.set("id", doc.id); //this is the package model ID
    							model.set("members", new Array()); //Reset the member list
    							model.getMembers();
    						}
    						//If there is no resource map, then this is the only document to in this package
    						else if((typeof doc.resourceMap === "undefined") || !doc.resourceMap){
    							model.set('id', null);
    							model.set('memberIds', new Array(doc.id));
    							model.set('members', [new SolrResult(doc)]);
    							model.trigger("change:members");
    							model.flagComplete();
    						}
    						else{
    							model.set('id', doc.resourceMap[0]);
    							model.getMembers();
    						}
    					}
    				}
    			}

    			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
    		},

    		/* Get all the members of a resource map/package based on the id attribute of this model.
    		 * Create a SolrResult model for each member and save it in the members[] attribute of this model. */
    		getMembers: function(options){
    			this.pending = true;

    			var model   = this,
    				members = [],
    				pids    = []; //Keep track of each object pid

    			//*** Find all the files that are a part of this resource map and the resource map itself
    			var provFlList = MetacatUI.appSearchModel.getProvFlList();
    			var query = 'fl=resourceMap,fileName,obsoletes,obsoletedBy,size,formatType,formatId,id,datasource,' +
    							'rightsHolder,dateUploaded,archived,title,origin,prov_instanceOfClass,isDocumentedBy,isPublic' +
      						'&rows=1000' +
      						'&q=%28resourceMap:%22' + encodeURIComponent(this.id) + '%22%20OR%20id:%22' + encodeURIComponent(this.id) + '%22%29' +
      						'&wt=json';

    			if( this.get("getArchivedMembers") ){
    				query += "&archived=archived:*";
    			}

    			var requestSettings = {
    				url: MetacatUI.appModel.get("queryServiceUrl") + query,
    				success: function(data, textStatus, xhr) {

    					//Separate the resource maps from the data/metadata objects
    					_.each(data.response.docs, function(doc){
    						if(doc.id == model.get("id")){
    							model.set("indexDoc", doc);
    							model.set(doc);
    							if(model.get("resourceMap") && (options && options.getParentMetadata))
    								model.getParentMetadata();
    						}
    						else{
    							pids.push(doc.id);

    							if(doc.formatType == "RESOURCE"){
    								var newPckg = new PackageModel(doc);
    								newPckg.set("parentPackage", model);
    								members.push(newPckg);
    							}
    							else
    								members.push(new SolrResult(doc));
    						}
    					});

    					model.set('memberIds', _.uniq(pids));
    					model.set('members', members);

    					if(model.getNestedPackages().length > 0)
    						model.createNestedPackages();
    					else
    						model.flagComplete();
    				}
    			}

    			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));

    			return this;
    		},

    		/*
    		 * Send custom options to the Backbone.Model.fetch() function
    		 */
    		fetch: function(options){
    			if(!options) var options = {};

    			var fetchOptions = _.extend({dataType: "text"}, options);

                //Add the authorization options
                fetchOptions = _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings());

                return Backbone.Model.prototype.fetch.call(this, fetchOptions);
    		},

    		/*
             * Deserialize a Package from OAI-ORE RDF XML
             */
            parse: function(response, options) {

                //Save the raw XML in case it needs to be used later
                this.set("objectXML", $.parseHTML(response));

                //Define the namespaces
                var RDF =     rdf.Namespace(this.namespaces.RDF),
                    FOAF =    rdf.Namespace(this.namespaces.FOAF),
                    OWL =     rdf.Namespace(this.namespaces.OWL),
                    DC =      rdf.Namespace(this.namespaces.DC),
                    ORE =     rdf.Namespace(this.namespaces.ORE),
                    DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
                    CITO =    rdf.Namespace(this.namespaces.CITO);

                var memberStatements = [],
                    memberURIParts,
                    memberPIDStr,
                    memberPID,
                    memberModel,
                    models = []; // the models returned by parse()

                try {
                    rdf.parse(response, this.dataPackageGraph, MetacatUI.appModel.get("objectServiceUrl") + (encodeURIComponent(this.id) || encodeURIComponent(this.seriesid)), 'application/rdf+xml');

                    // List the package members
                    memberStatements = this.dataPackageGraph.statementsMatching(
                        undefined, ORE('aggregates'), undefined, undefined);

                    var memberPIDs = [],
                    	members = [],
                    	currentMembers = this.get("members"),
                    	model = this;

                    // Get system metadata for each member to eval the formatId
                    _.each(memberStatements, function(memberStatement){
                        memberURIParts = memberStatement.object.value.split('/');
                        memberPIDStr = _.last(memberURIParts);
                        memberPID = decodeURIComponent(memberPIDStr);

                        if ( memberPID ){
                        	memberPIDs.push(memberPID);

                        	//Get the current model from the member list, if it exists
                        	var existingModel = _.find(currentMembers, function(m){
                        		return m.get("id") == decodeURIComponent(memberPID);
                        	});

                        	//Add the existing model to the new member list
                        	if(existingModel){
                        		members.push(existingModel);
                        	}
                        	//Or create a new SolrResult model
                        	else{
                        		members.push(new SolrResult({
                        			id: decodeURIComponent(memberPID)
                        		}));
                        	}
                        }

                    }, this);

                    //Get the documents relationships
                    var documentedByStatements = this.dataPackageGraph.statementsMatching(
                            undefined, CITO('isDocumentedBy'), undefined, undefined),
                        metadataPids = [];

                    _.each(documentedByStatements, function(statement){
                    	//Get the data object that is documentedBy metadata
                    	var dataPid = decodeURIComponent(_.last(statement.subject.value.split('/'))),
                    		dataObj = _.find(members, function(m){ return (m.get("id") == dataPid) }),
                    		metadataPid = _.last(statement.object.value.split('/'));

                    	//Save this as a metadata model
                    	metadataPids.push(metadataPid);

                    	//Set the isDocumentedBy field
                    	var isDocBy = dataObj.get("isDocumentedBy");
                    	if(isDocBy && Array.isArray(isDocBy)) isDocBy.push(metadataPid);
                    	else if(isDocBy && !Array.isArray(isDocBy)) isDocBy = [isDocBy, metadataPid];
                    	else isDocBy = [metadataPid];

                    	dataObj.set("isDocumentedBy", isDocBy);
                    }, this);

                    //Get the metadata models and mark them as metadata
                    var metadataModels = _.filter(members, function(m){ return _.contains(metadataPids, m.get("id")) });
                    _.invoke(metadataModels, "set", "formatType", "METADATA");

                    //Keep the pids in the collection for easy access later
                    this.set("memberIds", memberPIDs);
                    this.set("members", members);

                } catch (error) {
                    console.log(error);
                }
                return models;
            },

    		/*
    		 * Overwrite the Backbone.Model.save() function to set custom options
    		 */
    		save: function(attrs, options){
    			if(!options) var options = {};

    			//Get the system metadata first
    			if(!this.get("hasSystemMetadata")){
    				var model = this;
    				var requestSettings = {
    						url: MetacatUI.appModel.get("metaServiceUrl") + encodeURIComponent(this.get("id")),
    						success: function(response){
    							model.parseSysMeta(response);

    							model.set("hasSystemMetadata", true);
    							model.save.call(model, null, options);
    						},
    						dataType: "text"
    				}
    				$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
    				return;
    			}

    			//Create a new pid if we are updating the object
    			if(!options.sysMetaOnly){
    				//Set a new id
    				var oldPid = this.get("id");
    				this.set("oldPid", oldPid);
    				this.set("id", "urn:uuid:" + uuid.v4());
    				this.set("obsoletes", oldPid);
    				this.set("obsoletedBy", null);
    				this.set("archived", false);
    			}

    			//Create the system metadata
    			var sysMetaXML = this.serializeSysMeta();

    			//Send the new pid, old pid, and system metadata
    			var xmlBlob = new Blob([sysMetaXML], {type : 'application/xml'});
    			var formData = new FormData();
    			formData.append("sysmeta", xmlBlob, "sysmeta");

    			//Let's try updating the system metadata for now
    			if(options.sysMetaOnly){
    				formData.append("pid", this.get("id"));

    				var requestSettings = {
    						url: MetacatUI.appModel.get("metaServiceUrl"),
    						type: "PUT",
    						cache: false,
    					    contentType: false,
    					    processData: false,
    						data: formData,
    						success: function(response){
    						},
    						error: function(data){
    							console.log("error updating system metadata");
    						}
    				}
    				$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
    			}
    			else{
    				//Add the ids to the form data
    				formData.append("newPid", this.get("id"));
    				formData.append("pid", oldPid);

    				//Create the resource map XML
    				var mapXML = this.serialize();
    				var mapBlob = new Blob([mapXML], {type : 'application/xml'});
    				formData.append("object", mapBlob);

    				//Get the size of the new resource map
    				this.set("size", mapBlob.size);

    				//Get the new checksum of the resource map
    				var checksum = md5(mapXML);
    				this.set("checksum", checksum);

    				var requestSettings = {
    						url: MetacatUI.appModel.get("objectServiceUrl"),
    						type: "PUT",
    						cache: false,
    						contentType: false,
    						processData: false,
    						data: formData,
    						success: function(response){
    						},
    						error: function(data){
    							console.log("error udpating object");
    						}
    				}
    				$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
    			}
    		},

    		parseSysMeta: function(response){
            	this.set("sysMetaXML", $.parseHTML(response));

        		var responseDoc = $.parseHTML(response),
        			systemMetadata,
        			prependXML = "",
        			appendXML = "";

        		for(var i=0; i<responseDoc.length; i++){
        			if((responseDoc[i].nodeType == 1) && (responseDoc[i].localName.indexOf("systemmetadata") > -1))
        				systemMetadata = responseDoc[i];
        		}

        		//Parse the XML to JSON
        		var sysMetaValues = this.toJson(systemMetadata),
        			camelCasedValues = {};
        		//Convert the JSON to a camel-cased version, which matches Solr and is easier to work with in code
        		_.each(Object.keys(sysMetaValues), function(key){
        			camelCasedValues[this.sysMetaNodeMap[key]] = sysMetaValues[key];
        		}, this);

        		//Set the values on the model
        		this.set(camelCasedValues);
            },

            serialize: function(){
            	//Create an RDF serializer
            	var serializer = rdf.Serializer();
            	serializer.store = this.dataPackageGraph;

            	//Define the namespaces
                var ORE  = rdf.Namespace(this.namespaces.ORE),
                	CITO = rdf.Namespace(this.namespaces.CITO);

            	//Get the pid of this package - depends on whether we are updating or creating a resource map
                var pid = this.get("id"),
                	oldPid = this.get("oldPid"),
                	updating = oldPid? true : false;

                //Update the pids in the RDF graph only if we are updating the resource map with a new pid
                if(updating){

                	//Find the identifier statement in the resource map
    				var idNode =  rdf.lit(oldPid),
    					idStatement = this.dataPackageGraph.statementsMatching(undefined, undefined, idNode);

    				//Get the CN Resolve Service base URL from the resource map (mostly important in dev environments where it will not always be cn.dataone.org)
    				var	cnResolveUrl = idStatement[0].subject.value.substring(0, idStatement[0].subject.value.indexOf(oldPid));
    				this.dataPackageGraph.cnResolveUrl = cnResolveUrl;

    				//Create variations of the resource map ID using the resolve URL so we can always find it in the RDF graph
    	            var	oldPidVariations = [oldPid, encodeURIComponent(oldPid), cnResolveUrl+ encodeURIComponent(oldPid)];

                	//Get all the isAggregatedBy statements
    	            var aggregationNode =  rdf.sym(cnResolveUrl + encodeURIComponent(oldPid) + "#aggregation"),
    	            	aggByStatements = this.dataPackageGraph.statementsMatching(undefined, ORE("isAggregatedBy"));

    	            //Using the isAggregatedBy statements, find all the DataONE object ids in the RDF graph
    	            var idsFromXML = [];
    	            _.each(aggByStatements, function(statement){

    	            	//Check if the resource map ID is the old existing id, so we don't collect ids that are not about this resource map
    	            	if(_.find(oldPidVariations, function(oldPidV){ return (oldPidV + "#aggregation" == statement.object.value) })){
    	            		var statementID = statement.subject.value;
    	            		idsFromXML.push(statementID);

    	            		//Add variations of the ID so we make sure we account for all the ways they exist in the RDF XML
    	            		if(statementID.indexOf(cnResolveUrl) > -1)
    	            			idsFromXML.push(statementID.substring(statementID.lastIndexOf("/") + 1));
    		            	else
    		            		idsFromXML.push(cnResolveUrl + encodeURIComponent(statementID));
    	            	}

    	            }, this);

    	            //Get all the ids from this model
                	var idsFromModel = _.invoke(this.get("members"), "get", "id");

    		        //Find the difference between the model IDs and the XML IDs to get a list of added members
    	            var addedIds  = _.without(_.difference(idsFromModel, idsFromXML), oldPidVariations);
    	            //Create variations of all these ids too
    	            var allMemberIds = idsFromModel;
    	            _.each(idsFromModel, function(id){
    	            	allMemberIds.push(cnResolveUrl + encodeURIComponent(id));
    	           	});

                	//Remove any other isAggregatedBy statements that are not listed as members of this model
    	            _.each(aggByStatements, function(statement){
    	            	if(!_.contains(allMemberIds, statement.subject.value))
    	            		this.removeFromAggregation(statement.subject.value);
    	            	else if(_.find(oldPidVariations, function(oldPidV){ return (oldPidV + "#aggregation" == statement.object.value) }))
    	            		statement.object.value = cnResolveUrl+ encodeURIComponent(pid) + "#aggregation";
    	            }, this);

                	//Change all the statements in the RDF where the aggregation is the subject, to reflect the new resource map ID
    	            var aggregationSubjStatements = this.dataPackageGraph.statementsMatching(aggregationNode);
    	            _.each(aggregationSubjStatements, function(statement){
    	            	statement.subject.value = cnResolveUrl + encodeURIComponent(pid) + "#aggregation";
    	            });

                	//Change all the statements in the RDF where the aggregation is the object, to reflect the new resource map ID
    	            var aggregationObjStatements = this.dataPackageGraph.statementsMatching(undefined, undefined, aggregationNode);
    	            _.each(aggregationObjStatements, function(statement){
    	            	statement.object.value = cnResolveUrl+ encodeURIComponent(pid) + "#aggregation";
    	            });

    				//Change all the resource map subject nodes in the RDF graph
    				var rMapNode =  rdf.sym(cnResolveUrl + encodeURIComponent(oldPid));
    			    var rMapStatements = this.dataPackageGraph.statementsMatching(rMapNode);
    			    _.each(rMapStatements, function(statement){
    			    	statement.subject.value = cnResolveUrl + encodeURIComponent(pid);
    			    });

    			    //Change the idDescribedBy statement
    			    var isDescribedByStatements = this.dataPackageGraph.statementsMatching(undefined, ORE("isDescribedBy"), rdf.sym(oldPid));
    			    if(isDescribedByStatements[0])
    			    	isDescribedByStatements[0].object.value = pid;

                	//Add nodes for new package members
                	_.each(addedIds, function(id){
                		this.addToAggregation(id);
                	}, this);

    			    //Change all the resource map identifier literal node in the RDF graph
    				if(idStatement[0]) idStatement[0].object.value = pid;

                }

                //Now serialize the RDF XML
                var serializer = rdf.Serializer();
                serializer.store = this.dataPackageGraph;

                var xmlString = serializer.statementsToXML(this.dataPackageGraph.statements);

            	return xmlString;
            },

            serializeSysMeta: function(){
            	//Get the system metadata XML that currently exists in the system
            	var xml = $(this.get("sysMetaXML"));

            	//Update the system metadata values
            	xml.find("serialversion").text(this.get("serialVersion") || "0");
            	xml.find("identifier").text((this.get("newPid") || this.get("id")));
            	xml.find("formatid").text(this.get("formatId"));
            	xml.find("size").text(this.get("size"));
            	xml.find("checksum").text(this.get("checksum"));
            	xml.find("submitter").text(this.get("submitter") || MetacatUI.appUserModel.get("username"));
            	xml.find("rightsholder").text(this.get("rightsHolder") || MetacatUI.appUserModel.get("username"));
            	xml.find("archived").text(this.get("archived"));
            	xml.find("dateuploaded").text(this.get("dateUploaded") || new Date().toISOString());
            	xml.find("datesysmetadatamodified").text(this.get("dateSysMetadataModified") || new Date().toISOString());
            	xml.find("originmembernode").text(this.get("originMemberNode") || MetacatUI.nodeModel.get("currentMemberNode"));
            	xml.find("authoritativemembernode").text(this.get("authoritativeMemberNode") || MetacatUI.nodeModel.get("currentMemberNode"));

            	if(this.get("obsoletes"))
            		xml.find("obsoletes").text(this.get("obsoletes"));
            	else
            		xml.find("obsoletes").remove();

            	if(this.get("obsoletedBy"))
            		xml.find("obsoletedby").text(this.get("obsoletedBy"));
            	else
            		xml.find("obsoletedby").remove();

            	//Write the access policy
            	var accessPolicyXML = '<accessPolicy>\n';
            	_.each(this.get("accesspolicy"), function(policy, policyType, all){
        			var fullPolicy = all[policyType];

        			_.each(fullPolicy, function(policyPart){
        				accessPolicyXML += '\t<' + policyType + '>\n';

            			accessPolicyXML += '\t\t<subject>' + policyPart.subject + '</subject>\n';

            			var permissions = Array.isArray(policyPart.permission)? policyPart.permission : [policyPart.permission];
            			_.each(permissions, function(perm){
            				accessPolicyXML += '\t\t<permission>' + perm + '</permission>\n';
                		});

            			accessPolicyXML += '\t</' + policyType + '>\n';
        			});
            	});
            	accessPolicyXML += '</accessPolicy>';

            	//Replace the old access policy with the new one
            	xml.find("accesspolicy").replaceWith(accessPolicyXML);

            	var xmlString = $(document.createElement("div")).append(xml.clone()).html();

            	//Now camel case the nodes
            	_.each(Object.keys(this.sysMetaNodeMap), function(name, i, allNodeNames){
            		var regEx = new RegExp("<" + name, "g");
            		xmlString = xmlString.replace(regEx, "<" + this.sysMetaNodeMap[name]);
            		var regEx = new RegExp(name + ">", "g");
            		xmlString = xmlString.replace(regEx, this.sysMetaNodeMap[name] + ">");
            	}, this);

            	xmlString = xmlString.replace(/systemmetadata/g, "systemMetadata");

            	return xmlString;
            },

            //Adds a new object to the resource map RDF graph
            addToAggregation: function(id){
            	if(id.indexOf(this.dataPackageGraph.cnResolveUrl) < 0)
            		var fullID = this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);
            	else{
            		var fullID = id;
            		id = id.substring(this.dataPackageGraph.cnResolveUrl.lastIndexOf("/") + 1);
            	}

            	//Initialize the namespaces
            	var ORE     = rdf.Namespace(this.namespaces.ORE),
            		DCTERMS = rdf.Namespace(this.namespaces.DCTERMS),
            		XML     = rdf.Namespace(this.namespaces.XML),
            		CITO    = rdf.Namespace(this.namespaces.CITO);

            	//Create a node for this object, the identifier, the resource map, and the aggregation
            	var objectNode = rdf.sym(fullID),
            		mapNode    = rdf.sym(this.dataPackageGraph.cnResolveUrl + encodeURIComponent(this.get("id"))),
            		aggNode    = rdf.sym(this.dataPackageGraph.cnResolveUrl + encodeURIComponent(this.get("id")) + "#aggregation"),
            		idNode     = rdf.literal(id, undefined, XML("string"));

            	//Add the statement: this object isAggregatedBy the resource map aggregation
    			this.dataPackageGraph.addStatement(rdf.st(objectNode, ORE("isAggregatedBy"), aggNode));
    			//Add the statement: The resource map aggregation aggregates this object
    			this.dataPackageGraph.addStatement(rdf.st(aggNode, ORE("aggregates"), objectNode));
    			//Add the statement: This object has the identifier {id}
    			this.dataPackageGraph.addStatement(rdf.st(objectNode, DCTERMS("identifier"), idNode));

    			//Find the metadata doc that describes this object
    			var model   = _.find(this.get("members"), function(m){ return m.get("id") == id }),
    				isDocBy = model.get("isDocumentedBy");

    			//If this object is documented by any metadata...
    			if(isDocBy){
    				//Get the ids of all the metadata objects in this package
    				var	metadataInPackage = _.compact(_.map(this.get("members"), function(m){ if(m.get("formatType") == "METADATA") return m.get("id"); }));
    				//Find the metadata IDs that are in this package that also documents this data object
    				var metadataIds = Array.isArray(isDocBy)? _.intersection(metadataInPackage, isDocBy) : _.intersection(metadataInPackage, [isDocBy]);

    				//For each metadata that documents this object, add a CITO:isDocumentedBy and CITO:documents statement
    				_.each(metadataIds, function(metaId){
    					//Create the named nodes and statements
    					var memberNode       = rdf.sym(this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id)),
    						metadataNode     = rdf.sym(this.dataPackageGraph.cnResolveUrl + encodeURIComponent(metaId)),
    						isDocByStatement = rdf.st(memberNode, CITO("isDocumentedBy"), metadataNode),
    						documentsStatement = rdf.st(metadataNode, CITO("documents"), memberNode);
    					//Add the statements
    					this.dataPackageGraph.addStatement(isDocByStatement);
    					this.dataPackageGraph.addStatement(documentsStatement);
    				}, this);
    			}
            },

            removeFromAggregation: function(id){
            	if(!id.indexOf(this.dataPackageGraph.cnResolveUrl)) id = this.dataPackageGraph.cnResolveUrl + encodeURIComponent(id);

            	var removedObjNode = rdf.sym(id),
            		statements = _.union(this.dataPackageGraph.statementsMatching(undefined, undefined, removedObjNode),
            						this.dataPackageGraph.statementsMatching(removedObjNode));

    			this.dataPackageGraph.removeStatements(statements);
            },

    		getParentMetadata: function(){
    			var rMapIds = this.get("resourceMap");

    			//Create a query that searches for any resourceMap with an id matching one of the parents OR an id that matches one of the parents.
    			//This will return all members of the parent resource maps AND the parent resource maps themselves
    			var rMapQuery = "",
    				idQuery = "";
    			if(Array.isArray(rMapIds) && (rMapIds.length > 1)){
    				_.each(rMapIds, function(id, i, ids){

    					//At the begininng of the list of ids
    					if(rMapQuery.length == 0){
    						rMapQuery += "resourceMap:(";
    						idQuery += "id:(";
    					}

    					//The id
    					rMapQuery += "%22" + encodeURIComponent(id) + "%22";
    					idQuery   += "%22" + encodeURIComponent(id) + "%22";

    					//At the end of the list of ids
    					if(i+1 == ids.length){
    						rMapQuery += ")";
    						idQuery += ")";
    					}
    					//In-between each id
    					else{
    						rMapQuery += " OR ";
    						idQuery += " OR ";
    					}
    				});
    			}
    			else{
    				//When there is just one parent, the query is simple
    				var rMapId = Array.isArray(rMapIds)? rMapIds[0] : rMapIds;
    				rMapQuery += "resourceMap:%22" + encodeURIComponent(rMapId) + "%22";
    				idQuery   += "id:%22" + encodeURIComponent(rMapId) + "%22";
    			}
    			var query = "fl=title,id,obsoletedBy,resourceMap" +
    						"&wt=json" +
    						"&group=true&group.field=formatType&group.limit=-1" +
    						"&q=((formatType:METADATA AND " + rMapQuery + ") OR " + idQuery + ")";

    			var model = this;
    			var requestSettings = {
    				url: MetacatUI.appModel.get("queryServiceUrl") + query,
    				success: function(data, textStatus, xhr) {
    					var results = data.grouped.formatType.groups,
    							resourceMapGroup = _.where(results, { groupValue: "RESOURCE" })[0],
    						rMapList = resourceMapGroup? resourceMapGroup.doclist : null,
    						rMaps = rMapList? rMapList.docs : [],
    						rMapIds = _.pluck(rMaps, "id"),
    						parents = [],
    						parentIds = [];

    					//As long as this map isn't obsoleted by another map in our results list, we will show it
    					_.each(rMaps, function(map){
    						if(! (map.obsoletedBy && _.contains(rMapIds, map.obsoletedBy))){
    							parents.push(map);
    							parentIds.push(map.id);
    						}
    					});

    					var metadataList =  _.where(results, {groupValue: "METADATA"})[0],
    						metadata = (metadataList && metadataList.doclist)? metadataList.doclist.docs : [],
    						metadataModels = [];

                //As long as this map isn't obsoleted by another map in our results list, we will show it
      					_.each(metadata, function(m){

                  //Find the metadata doc that obsoletes this one
                  var isObsoletedBy = _.findWhere(metadata, { id: m.obsoletedBy });

                  //If one isn't found, then this metadata doc is the most recent
      						if(typeof isObsoletedBy == "undefined"){
                    //If this metadata doc is in one of the filtered parent resource maps
        						if(_.intersection(parentIds, m.resourceMap).length){
                      //Create a SolrResult model and add to an array
        							metadataModels.push(new SolrResult(m));
                    }
      						}
      					});

    					model.set("parentPackageMetadata", metadataModels);
    					model.trigger("change:parentPackageMetadata");
    				}
    			}

    			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
    		},

    		//Create the URL string that is used to download this package
    		getURL: function(){
    			var url = null;

    			//If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from
    			if((MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") > -1) && MetacatUI.nodeModel.get("members").length){
    				var source = this.get("datasource"),
    					node   = _.find(MetacatUI.nodeModel.get("members"), {identifier: source});

    				//If this node has MNRead v2 services...
    				if(node && node.readv2)
    					url = node.baseURL + "/v2/packages/application%2Fbagit-097/" + encodeURIComponent(this.get("id"));
    			}
    			else if(MetacatUI.appModel.get("packageServiceUrl"))
    				url = MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(this.get("id"));

    			this.set("url", url);
    			return url;
    		},

    		createNestedPackages: function(){
    			var parentPackage = this,
    			    nestedPackages = this.getNestedPackages(),
    					numNestedPackages = nestedPackages.length,
    					numComplete = 0;

    			_.each(nestedPackages, function(nestedPackage, i, nestedPackages){

    				//Flag the parent model as complete when all the nested package info is ready
    				nestedPackage.on("complete", function(){
    					numComplete++;

    					//This is the last package in this package - finish up details and flag as complete
    					if(numNestedPackages == numComplete){
    						var sorted = _.sortBy(parentPackage.get("members"), function(p){ return p.get("id") });
    						parentPackage.set("members", sorted);
    						parentPackage.flagComplete();
    					}
    				});

    				//Only look one-level deep at all times to avoid going down a rabbit hole
    				if( nestedPackage.get("parentPackage") && nestedPackage.get("parentPackage").get("parentPackage") ){
    					nestedPackage.flagComplete();
    					return;
    				}
    				else{
    					//Get the members of this nested package
    					nestedPackage.getMembers();
    				}

    			});
    		},

    		getNestedPackages: function(){
    			return _.where(this.get("members"), {type: "Package"});
    		},

    		getMemberNames: function(){
    			var metadata = this.getMetadata();
    			if(!metadata) return false;

    			//Load the rendered metadata from the view service
    			var viewService = MetacatUI.appModel.get("viewServiceUrl") + encodeURIComponent(metadata.get("id"));
    			var requestSettings = {
    					url: viewService,
    					success: function(data, response, xhr){
    						if(solrResult.get("formatType") == "METADATA")
    							entityName = solrResult.get("title");
    						else{
    							var container = viewRef.findEntityDetailsContainer(solrResult.get("id"));
    							if(container && container.length > 0){
    								var entityName = $(container).find(".entityName").attr("data-entity-name");
    								if((typeof entityName === "undefined") || (!entityName)){
    									entityName = $(container).find(".control-label:contains('Entity Name') + .controls-well").text();
    									if((typeof entityName === "undefined") || (!entityName))
    										entityName = null;
    								}
    							}
    							else
    								entityName = null;

    						}
    					}
    			}
    			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
    		},

    		/*
    		 * Will query for the derivations of this package, and sort all entities in the prov trace
    		 * into sources and derivations.
    		 */
    		getProvTrace: function(){
    			var model = this;

    			//See if there are any prov fields in our index before continuing
    			if(!MetacatUI.appSearchModel.getProvFields()) return this;

    			//Start keeping track of the sources and derivations
    			var sources 		   = new Array(),
    				derivations 	   = new Array();

    			//Search for derivations of this package
    			var derivationsQuery = MetacatUI.appSearchModel.getGroupedQuery("prov_wasDerivedFrom",
    					_.map(this.get("members"), function(m){ return m.get("id"); }), "OR") +
    					"%20-obsoletedBy:*";

    			var requestSettings = {
    					url: MetacatUI.appModel.get("queryServiceUrl") + "&q=" + derivationsQuery + "&wt=json&rows=1000" +
    						 "&fl=id,resourceMap,documents,isDocumentedBy,prov_wasDerivedFrom",
    					success: function(data){
    						_.each(data.response.docs, function(result){
    							derivations.push(result.id);
    						});

    						//Make arrays of unique IDs of objects that are sources or derivations of this package.
    						_.each(model.get("members"), function(member, i){
    							if(member.type == "Package") return;

    							if(member.hasProvTrace()){
    								sources 	= _.union(sources, member.getSources());
    								derivations = _.union(derivations, member.getDerivations());
    							}
    						});

    						//Save the arrays of sources and derivations
    						model.set("sources", sources);
    						model.set("derivations", derivations);

    						//Now get metadata about all the entities in the prov trace not in this package
    						model.getExternalProvTrace();
    					}
    			}
    			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
    		},

    		getExternalProvTrace: function(){
    			var model = this;

    			//Compact our list of ids that are in the prov trace by combining the sources and derivations and removing ids of members of this package
    			var externalProvEntities = _.difference(_.union(this.get("sources"), this.get("derivations")), this.get("memberIds"));

    			//If there are no sources or derivations, then we do not need to find resource map ids for anything
    			if(!externalProvEntities.length){

    				//Save this prov trace on a package-member/document/object level.
    				if(this.get("sources").length || this.get("derivations").length)
    					this.setMemberProvTrace();

    				//Flag that the provenance trace is complete
    				this.set("provenanceFlag", "complete");

    				return this;
    			}
    			else{
    				//Create a query where we retrieve the ID of the resource map of each source and derivation
    				var idQuery = MetacatUI.appSearchModel.getGroupedQuery("id", externalProvEntities, "OR");

    				//Create a query where we retrieve the metadata for each source and derivation
    				var metadataQuery = MetacatUI.appSearchModel.getGroupedQuery("documents", externalProvEntities, "OR");
    			}

    			//TODO: Find the products of programs/executions

    			//Make a comma-separated list of the provenance field names
    			var provFieldList = "";
    			_.each(MetacatUI.appSearchModel.getProvFields(), function(fieldName, i, list){
    				provFieldList += fieldName;
    				if(i < list.length-1) provFieldList += ",";
    			});

    			//Combine the two queries with an OR operator
    			if(idQuery.length && metadataQuery.length) var combinedQuery = idQuery + "%20OR%20" + metadataQuery;
    			else return this;

    			//the full and final query in Solr syntax
    			var query = "q=" + combinedQuery +
    						"&fl=id,resourceMap,documents,isDocumentedBy,formatType,formatId,dateUploaded,rightsHolder,datasource,prov_instanceOfClass," +
    						provFieldList +
    						"&rows=100&wt=json";

    			//Send the query to the query service
    			var requestSettings = {
    				url: MetacatUI.appModel.get("queryServiceUrl") + query,
    				success: function(data, textStatus, xhr){

    					//Do any of our docs have multiple resource maps?
    					var hasMultipleMaps = _.filter(data.response.docs, function(doc){
    						return((typeof doc.resourceMap !== "undefined") && (doc.resourceMap.length > 1))
    						});
    					//If so, we want to find the latest version of each resource map and only represent that one in the Prov Chart
    					if(typeof hasMultipleMaps !== "undefined"){
    						var allMapIDs = _.uniq(_.flatten(_.pluck(hasMultipleMaps, "resourceMap")));
    						if(allMapIDs.length){

    							var query = "q=+-obsoletedBy:*+" + MetacatUI.appSearchModel.getGroupedQuery("id", allMapIDs, "OR") +
    										"&fl=obsoletes,id" +
    										"&wt=json";
    							var requestSettings = {
    								url: MetacatUI.appModel.get("queryServiceUrl") + query,
    								success: function(mapData, textStatus, xhr){

    									//Create a list of resource maps that are not obsoleted by any other resource map retrieved
    									var resourceMaps = mapData.response.docs;

    									model.obsoletedResourceMaps = _.pluck(resourceMaps, "obsoletes");
    									model.latestResourceMaps    = _.difference(resourceMaps, model.obsoletedResourceMaps);

    									model.sortProvTrace(data.response.docs);
    								}
    							}
    							$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
    						}
    						else
    							model.sortProvTrace(data.response.docs);
    					}
    					else
    						model.sortProvTrace(data.response.docs);

    				}
    			}

    			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));

    			return this;
    		},

    		sortProvTrace: function(docs){
    			var model = this;

    			//Start an array to hold the packages in the prov trace
    			var sourcePackages   = new Array(),
    				derPackages      = new Array(),
    				sourceDocs		 = new Array(),
    				derDocs	 		 = new Array(),
    				sourceIDs        = this.get("sources"),
    				derivationIDs    = this.get("derivations");

    			//Separate the results into derivations and sources and group by their resource map.
    			_.each(docs, function(doc, i){

    				var docModel = new SolrResult(doc),
    				      mapIds = docModel.get("resourceMap");

    				if(((typeof mapIds === "undefined") || !mapIds) && (docModel.get("formatType") == "DATA") && ((typeof docModel.get("isDocumentedBy") === "undefined") || !docModel.get("isDocumentedBy"))){
    					//If this object is not in a resource map and does not have metadata, it is a "naked" data doc, so save it by itself
    					if(_.contains(sourceIDs, doc.id))
    						sourceDocs.push(docModel);
    					if(_.contains(derivationIDs, doc.id))
    						derDocs.push(docModel);
    				}
    				else if(((typeof mapIds === "undefined") || !mapIds) && (docModel.get("formatType") == "DATA") && docModel.get("isDocumentedBy")){
    					//If this data doc does not have a resource map but has a metadata doc that documents it, create a blank package model and save it
    					var p = new PackageModel({
    						members: new Array(docModel)
    					});
    					//Add this package model to the sources and/or derivations packages list
    					if(_.contains(sourceIDs, docModel.get("id")))
    						sourcePackages[docModel.get("id")] = p;
    					if(_.contains(derivationIDs, docModel.get("id")))
    						derPackages[docModel.get("id")] = p;
    				}
    				else if(mapIds.length){
    					//If this doc has a resource map, create a package model and SolrResult model and store it
    					var id     = docModel.get("id");

    					//Some of these objects may have multiple resource maps
    					_.each(mapIds, function(mapId, i, list){

    						if(!_.contains(model.obsoletedResourceMaps, mapId)){
    							var documentsSource, documentsDerivation;
    							if(docModel.get("formatType") == "METADATA"){
    								if(_.intersection(docModel.get("documents"), sourceIDs).length)     documentsSource = true;
    								if(_.intersection(docModel.get("documents"), derivationIDs).length) documentsDerivation = true;
    							}

    							//Is this a source object or a metadata doc of a source object?
    							if(_.contains(sourceIDs, id) || documentsSource){
    								//Have we encountered this source package yet?
    								if(!sourcePackages[mapId] && (mapId != model.get("id"))){
    									//Now make a new package model for it
    									var p = new PackageModel({
    										id: mapId,
    										members: new Array(docModel)
    									});
    									//Add to the array of source packages
    									sourcePackages[mapId] = p;
    								}
    								//If so, add this member to its package model
    								else if(mapId != model.get("id")){
    									var memberList = sourcePackages[mapId].get("members");
    									memberList.push(docModel);
    									sourcePackages[mapId].set("members", memberList);
    								}
    							}

    							//Is this a derivation object or a metadata doc of a derivation object?
    							if(_.contains(derivationIDs, id) || documentsDerivation){
    								//Have we encountered this derivation package yet?
    								if(!derPackages[mapId] && (mapId != model.get("id"))){
    									//Now make a new package model for it
    									var p = new PackageModel({
    										id: mapId,
    										members: new Array(docModel)
    									});
    									//Add to the array of source packages
    									derPackages[mapId] = p;
    								}
    								//If so, add this member to its package model
    								else if(mapId != model.get("id")){
    									var memberList = derPackages[mapId].get("members");
    									memberList.push(docModel);
    									derPackages[mapId].set("members", memberList);
    								}
    							}
    						}
    					});
    				}
    			});

    			//Transform our associative array (Object) of packages into an array
    			var newArrays = new Array();
    			_.each(new Array(sourcePackages, derPackages, sourceDocs, derDocs), function(provObject){
    				var newArray = new Array(), key;
    				for(key in provObject){
    					newArray.push(provObject[key]);
    				}
    				newArrays.push(newArray);
    			});

    			//We now have an array of source packages and an array of derivation packages.
    			model.set("sourcePackages", newArrays[0]);
    			model.set("derivationPackages", newArrays[1]);
    			model.set("sourceDocs", newArrays[2]);
    			model.set("derivationDocs", newArrays[3]);

    			//Save this prov trace on a package-member/document/object level.
    			model.setMemberProvTrace();

    			//Flag that the provenance trace is complete
    			model.set("provenanceFlag", "complete");
    		},

    		setMemberProvTrace: function(){
    			var model = this,
    				relatedModels = this.get("relatedModels"),
    				relatedModelIDs = new Array();

    			//Now for each doc, we want to find which member it is related to
    			_.each(this.get("members"), function(member, i, members){
    				if(member.type == "Package") return;

    				//Get the sources and derivations of this member
    				var memberSourceIDs = member.getSources();
    				var memberDerIDs    = member.getDerivations();

    				//Look through each source package, derivation package, source doc, and derivation doc.
    				_.each(model.get("sourcePackages"), function(pkg, i){
    					_.each(pkg.get("members"), function(sourcePkgMember, i){
    						//Is this package member a direct source of this package member?
    						if(_.contains(memberSourceIDs, sourcePkgMember.get("id")))
    							//Save this source package member as a source of this member
    							member.set("provSources", _.union(member.get("provSources"), [sourcePkgMember]));

    						//Save this in the list of related models
    						if(!_.contains(relatedModelIDs, sourcePkgMember.get("id"))){
    							relatedModels.push(sourcePkgMember);
    							relatedModelIDs.push(sourcePkgMember.get("id"));
    						}
    					});
    				});
    				_.each(model.get("derivationPackages"), function(pkg, i){
    					_.each(pkg.get("members"), function(derPkgMember, i){
    						//Is this package member a direct source of this package member?
    						if(_.contains(memberDerIDs, derPkgMember.get("id")))
    							//Save this derivation package member as a derivation of this member
    							member.set("provDerivations", _.union(member.get("provDerivations"), [derPkgMember]));

    						//Save this in the list of related models
    						if(!_.contains(relatedModelIDs, derPkgMember.get("id"))){
    							relatedModels.push(derPkgMember);
    							relatedModelIDs.push(derPkgMember.get("id"));
    						}
    					});
    				});
    				_.each(model.get("sourceDocs"), function(doc, i){
    					//Is this package member a direct source of this package member?
    					if(_.contains(memberSourceIDs, doc.get("id")))
    						//Save this source package member as a source of this member
    						member.set("provSources", _.union(member.get("provSources"), [doc]));

    					//Save this in the list of related models
    					if(!_.contains(relatedModelIDs, doc.get("id"))){
    						relatedModels.push(doc);
    						relatedModelIDs.push(doc.get("id"));
    					}
    				});
    				_.each(model.get("derivationDocs"), function(doc, i){
    					//Is this package member a direct derivation of this package member?
    					if(_.contains(memberDerIDs, doc.get("id")))
    						//Save this derivation package member as a derivation of this member
    						member.set("provDerivations", _.union(member.get("provDerivations"), [doc]));

    					//Save this in the list of related models
    					if(!_.contains(relatedModelIDs, doc.get("id"))){
    						relatedModels.push(doc);
    						relatedModelIDs.push(doc.get("id"));
    					}
    				});
    				_.each(members, function(otherMember, i){
    					//Is this other package member a direct derivation of this package member?
    					if(_.contains(memberDerIDs, otherMember.get("id")))
    						//Save this other derivation package member as a derivation of this member
    						member.set("provDerivations", _.union(member.get("provDerivations"), [otherMember]));
    					//Is this other package member a direct source of this package member?
    					if(_.contains(memberSourceIDs, otherMember.get("id")))
    						//Save this other source package member as a source of this member
    						member.set("provSources", _.union(member.get("provSources"), [otherMember]));

    					//Is this other package member an indirect source or derivation?
    					if((otherMember.get("type") == "program") && (_.contains(member.get("prov_generatedByProgram"), otherMember.get("id")))){
    						var indirectSources = _.filter(members, function(m){
    							return _.contains(otherMember.getInputs(), m.get("id"));
    						});
    						indirectSourcesIds = _.each(indirectSources, function(m){ return m.get("id") });
    						member.set("prov_wasDerivedFrom", _.union(member.get("prov_wasDerivedFrom"), indirectSourcesIds));
    						//otherMember.set("prov_hasDerivations", _.union(otherMember.get("prov_hasDerivations"), [member.get("id")]));
    						member.set("provSources", _.union(member.get("provSources"), indirectSources));
    					}
    					if((otherMember.get("type") == "program") && (_.contains(member.get("prov_usedByProgram"), otherMember.get("id")))){
    						var indirectDerivations = _.filter(members, function(m){
    							return _.contains(otherMember.getOutputs(), m.get("id"));
    						});
    						indirectDerivationsIds = _.each(indirectDerivations, function(m){ return m.get("id") });
    						member.set("prov_hasDerivations", _.union(member.get("prov_hasDerivations"), indirectDerivationsIds));
    						//otherMember.set("prov_wasDerivedFrom", _.union(otherMember.get("prov_wasDerivedFrom"), [member.get("id")]));
    						member.set("provDerivations", _.union(member.get("provDerivations"), indirectDerivationsIds));
    					}
    				});

    				//Add this member to the list of related models
    				if(!_.contains(relatedModelIDs, member.get("id"))){
    					relatedModels.push(member);
    					relatedModelIDs.push(member.get("id"));
    				}

    				//Clear out any duplicates
    				member.set("provSources", _.uniq(member.get("provSources")));
    				member.set("provDerivations", _.uniq(member.get("provDerivations")));
    			});

    			//Update the list of related models
    			this.set("relatedModels", relatedModels);

    		},

    		downloadWithCredentials: function(){
    			//Get info about this object
    			var	url = this.get("url"),
    				  model = this;

    			//Create an XHR
    			var xhr = new XMLHttpRequest();
    			xhr.withCredentials = true;

    			//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
    			xhr.onload = function(){

            //Get the file name from the Content-Disposition header
            var filename = xhr.getResponseHeader('Content-Disposition');

            //As a backup, use the system metadata file name or the id
            if(!filename){
              filename = model.get("filename") || model.get("id");
            }

            //Add a ".zip" extension if it doesn't exist
      			if( filename.indexOf(".zip") < 0 || (filename.indexOf(".zip") != (filename.length-4)) ){
              filename += ".zip";
            }

    			   //For IE, we need to use the navigator API
    			   if (navigator && navigator.msSaveOrOpenBlob) {
    				   navigator.msSaveOrOpenBlob(xhr.response, filename);
    			   }
    			   else{
    					var a = document.createElement('a');
    					a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob
    					a.download = filename; // Set the file name.
    					a.style.display = 'none';
    					document.body.appendChild(a);
    					a.click();
    					a.remove();
    			   }

             //Send this exception to Google Analytics
             if(MetacatUI.appModel.get("googleAnalyticsKey") && (typeof ga !== "undefined")){
               ga("send", "event", "download", "Download Package", model.get("id"));
             }

    			    model.trigger("downloadComplete");
    			};

    			xhr.onprogress = function(e){
    			    if (e.lengthComputable){
    			        var percent = (e.loaded / e.total) * 100;
    			        model.set("downloadPercent", percent);
    			    }
    			};

          xhr.onerror = function(e){
            model.trigger("downloadError");

            //Send this exception to Google Analytics
            if(MetacatUI.appModel.get("googleAnalyticsKey") && (typeof ga !== "undefined")){
              ga("send", "exception", {
                "exDescription": "Download package error: " + (e || "") +
                  " | Id: " + model.get("id") + " | v. " + MetacatUI.metacatUIVersion,
                "exFatal": true
              });
            }
          };

    			//Open and send the request with the user's auth token
    			xhr.open('GET', url);
    			xhr.responseType = "blob";
    			xhr.setRequestHeader("Authorization", "Bearer " + MetacatUI.appUserModel.get("token"));
    			xhr.send();
    		},

    		/* Returns the SolrResult that represents the metadata doc */
    		getMetadata: function(){
    			var members = this.get("members");
    			for(var i=0; i<members.length; i++){
    				if(members[i].get("formatType") == "METADATA") return members[i];
    			}

    			//If there are no metadata objects in this package, make sure we have searched for them already
    			if(!this.complete && !this.pending) this.getMembers();

    			return false;
    		},

    		//Check authority of the Metadata SolrResult model instead
    		checkAuthority: function(){

    			//Call the auth service
    			var authServiceUrl = MetacatUI.appModel.get('authServiceUrl');
    			if(!authServiceUrl) return false;

    			var model = this;

    			var requestSettings = {
    				url: authServiceUrl + encodeURIComponent(this.get("id")) + "?action=write",
    				type: "GET",
    				success: function(data, textStatus, xhr) {
    					model.set("isAuthorized", true);
    					model.trigger("change:isAuthorized");
    				},
    				error: function(xhr, textStatus, errorThrown) {
    					model.set("isAuthorized", false);
    				}
    			}
    			$.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
    		},

    		flagComplete: function(){
    			this.complete = true;
    			this.pending = false;
    			this.trigger("complete", this);
    		},

    		    /*
    		    * function xmlToJson - A utility function for converting XML to JSON
    		    *
    		    * @param xml {DOM Element} - An XML or HTML DOM element to convert to json
    		    * @returns {object} - A literal JS object that represents the given XML
    		    */
            toJson: function(xml) {

            	// Create the return object
            	var obj = {};

            	// do children
            	if (xml.hasChildNodes()) {
            		for(var i = 0; i < xml.childNodes.length; i++) {
            			var item = xml.childNodes.item(i);

            			//If it's an empty text node, skip it
            			if((item.nodeType == 3) && (!item.nodeValue.trim()))
            				continue;

            			//Get the node name
            			var nodeName = item.localName;

            			//If it's a new container node, convert it to JSON and add as a new object attribute
            			if((typeof(obj[nodeName]) == "undefined") && (item.nodeType == 1)) {
            				obj[nodeName] = this.toJson(item);
            			}
            			//If it's a new text node, just store the text value and add as a new object attribute
            			else if((typeof(obj[nodeName]) == "undefined") && (item.nodeType == 3)){
            				obj = item.nodeValue;
            			}
            			//If this node name is already stored as an object attribute...
            			else if(typeof(obj[nodeName]) != "undefined"){
            				//Cache what we have now
            				var old = obj[nodeName];
            				if(!Array.isArray(old))
            					old = [old];

            				//Create a new object to store this node info
            				var newNode = {};

            				//Add the new node info to the existing array we have now
        					if(item.nodeType == 1){
        						newNode = this.toJson(item);
        						var newArray = old.concat(newNode);
        					}
        					else if(item.nodeType == 3){
        						newNode = item.nodeValue;
        						var newArray = old.concat(newNode);
        					}

                			//Store the attributes for this node
                			_.each(item.attributes, function(attr){
                				newNode[attr.localName] = attr.nodeValue;
                			});

                			//Replace the old array with the updated one
        					obj[nodeName] = newArray;

        					//Exit
        					continue;
            			}

            			//Store the attributes for this node
            			/*_.each(item.attributes, function(attr){
            				obj[nodeName][attr.localName] = attr.nodeValue;
            			});*/

        			}

            	}
            	return obj;
            },

    		//Sums up the byte size of each member
    		getTotalSize: function(){
    			if(this.get("totalSize")) return this.get("totalSize");

    			if(this.get("members").length == 1){
    				var totalSize = this.get("members")[0].get("size");
    			}
    			else{
    				var totalSize = _.reduce(this.get("members"), function(sum, member){
    					if(typeof sum == "object")
    						sum = sum.get("size");

    					return sum + member.get("size");
    				});
    			}

    			this.set("totalSize", totalSize);
    			return totalSize;
    		},

    		/****************************/
    		/**
    		 * Convert number of bytes into human readable format
    		 *
    		 * @param integer bytes     Number of bytes to convert
    		 * @param integer precision Number of digits after the decimal separator
    		 * @return string
    		 */
    		bytesToSize: function(bytes, precision){
    		    var kilobyte = 1024;
    		    var megabyte = kilobyte * 1024;
    		    var gigabyte = megabyte * 1024;
    		    var terabyte = gigabyte * 1024;

    		    if(typeof bytes === "undefined") var bytes = this.get("size");

    		    if ((bytes >= 0) && (bytes < kilobyte)) {
    		        return bytes + ' B';

    		    } else if ((bytes >= kilobyte) && (bytes < megabyte)) {
    		        return (bytes / kilobyte).toFixed(precision) + ' KB';

    		    } else if ((bytes >= megabyte) && (bytes < gigabyte)) {
    		        return (bytes / megabyte).toFixed(precision) + ' MB';

    		    } else if ((bytes >= gigabyte) && (bytes < terabyte)) {
    		        return (bytes / gigabyte).toFixed(precision) + ' GB';

    		    } else if (bytes >= terabyte) {
    		        return (bytes / terabyte).toFixed(precision) + ' TB';

    		    } else {
    		        return bytes + ' B';
    		    }
    		}

    	});
    	return PackageModel;
    });