Source: 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;
});