# Source: models/DataONEObject.js

    /* global define */
    define(['jquery', 'underscore', 'backbone', 'uuid', 'he', 'collections/AccessPolicy', 'collections/ObjectFormats', 'md5'],
        function($, _, Backbone, uuid, he, AccessPolicy, ObjectFormats, md5){

            /*
             A DataONEObject represents a DataONE object that has a format
             type of DATA, METADATA, or RESOURCE.  It stores the system
             metadata attributes for the object at a minimum.
             TODO: incorporate Backbone.UniqueModel
            */
            var DataONEObject = Backbone.Model.extend(
              /** @lends DataONEObject.prototype */{

              type: "DataONEObject",
              selectedInEditor: false, // Has this package member been selected and displayed in the provenance editor?
              PROV:    "http://www.w3.org/ns/prov#",
              PROVONE: "http://purl.dataone.org/provone/2015/01/15/ontology#",

              defaults: function(){
                return{
                        // System Metadata attributes
                        serialVersion: null,
                        identifier: null,
                        formatId: null,
                        size: null,
                        checksum: null,
                        originalChecksum: null,
                        checksumAlgorithm: "MD5",
                        submitter: null,
                        rightsHolder : null,
                        accessPolicy: [], //An array of accessPolicy literal JS objects
                        replicationAllowed: null,
                        replicationPolicy: [],
                        obsoletes: null,
                        obsoletedBy: null,
                        archived: null,
                        dateUploaded: null,
                        dateSysMetadataModified: null,
                        originMemberNode: null,
                        authoritativeMemberNode: null,
                        replica: [],
                        seriesId: null, // uuid.v4(), (decide if we want to auto-set this)
                        mediaType: null,
                        fileName: null,
                        // Non-system metadata attributes:
                        datasource: null,
                        insert_count_i: null,
                        read_count_i: null,
                        changePermission: null,
                        writePermission: null,
                        readPermission: null,
                        isPublic: null,
                        dateModified: null,
                        id: "urn:uuid:" + uuid.v4(),
                        sizeStr: null,
                        type: "", // Data, Metadata, or DataPackage
                        formatType: "",
                        metadataEntity: null, // A model that represents the metadata for this file, e.g. an EMLEntity model
                        latestVersion: null,
                        isDocumentedBy: null,
                        documents: [],
                        resourceMap: [],
                        nodeLevel: 0, // Indicates hierarchy level in the view for indentation
                        sortOrder: null, // Metadata: 1, Data: 2, DataPackage: 3
                        synced: false, // True if the full model has been synced
                        uploadStatus: null, //c=complete, p=in progress, q=queued, e=error, no upload status=not in queue
                        uploadProgress: null,
                        sysMetaUploadStatus: null, //c=complete, p=in progress, q=queued, e=error, no upload status=not in queue
                        percentLoaded: 0, // Percent the file is read before caclculating the md5 sum
                        uploadFile: null, // The file reference to be uploaded (JS object: File)
                        errorMessage: null,
                        numSaveAttempts: 0,
                        notFound: false, //Whether or not this object was found in the system
                        originalAttrs: [], // An array of original attributes in a DataONEObject
                        changed: false, // If any attributes have been changed, including attrs in nested objects
                        hasContentChanges: false, // If attributes outside of originalAttrs have been changed
                        sysMetaXML: null, // A cached original version of the fetched system metadata document
                        objectXML: null, // A cached version of the object fetched from the server
                        isAuthorized: null, // If the stated permission is authorized by the user
                        createSeriesId: false, //If true, a seriesId will be created when this object is saved.
                        collections: [], //References to collections that this model is in
                        provSources: [],
                        provDerivations: [],
                        prov_generated: [],
                        prov_generatedByExecution: [],
                        prov_generatedByProgram: [],
                        prov_generatedByUser: [],
                        prov_hasDerivations: [],
                        prov_hasSources: [],
                        prov_instanceOfClass: [],
                        prov_used: [],
                        prov_usedByExecution: [],
                        prov_usedByProgram: [],
                        prov_usedByUser: [],
                        prov_wasDerivedFrom: [],
                        prov_wasExecutedByExecution: [],
                        prov_wasExecutedByUser: [],
                        prov_wasInformedBy: []
                    }
              },

              initialize: function(attrs, options) {
                if(typeof attrs == "undefined") var attrs = {};

                this.set("accessPolicy", this.createAccessPolicy());

                this.on("change:size", this.bytesToSize);
                if(attrs.size)
                    this.bytesToSize();

                // Cache an array of original attribute names to help in handleChange()
                if(this.type == "DataONEObject")
                  this.set("originalAttrs", Object.keys(this.attributes));
                else
                  this.set("originalAttrs", Object.keys(DataONEObject.prototype.defaults()));

                this.on("successSaving", this.updateRelationships);

                //Save a reference to this DataONEObject model in the metadataEntity model
                //whenever the metadataEntity is set
                this.on("change:metadataEntity", function(){
                  var entityMetadataModel = this.get("metadataEntity");

                  if( entityMetadataModel )
                    entityMetadataModel.set("dataONEObject", this);

                });

                this.on("sync", function(){
                  this.set("synced", true);
                });

              },

              /*
               * Maps the lower-case sys meta node names (valid in HTML DOM) to the
               * camel-cased sys meta node names (valid in DataONE).
               * Used during parse() and serialize()
               */
              nodeNameMap: function(){
                return{
              accesspolicy: "accessPolicy",
              accessrule: "accessRule",
              authoritativemembernode: "authoritativeMemberNode",
              checksumalgorithm: "checksumAlgorithm",
              dateuploaded: "dateUploaded",
              datesysmetadatamodified: "dateSysMetadataModified",
              formatid: "formatId",
              filename: "fileName",
              nodereference: "nodeReference",
              numberreplicas: "numberReplicas",
              obsoletedby: "obsoletedBy",
              originmembernode: "originMemberNode",
              replicamembernode: "replicaMemberNode",
              replicationallowed: "replicationAllowed",
              replicationpolicy: "replicationPolicy",
              replicationstatus: "replicationStatus",
              replicaverified: "replicaVerified",
              rightsholder: "rightsHolder",
              serialversion: "serialVersion",
                      seriesid: "seriesId"
                };
              },

              /* Provide the model URL on the server based on the newness of the object */
              url: function(){

                // With no id, we can't do anything
                if( !this.get("id") && !this.get("seriesid") )
                  return "";

                // Determine if we're updating a new/existing object,
                // or just its system metadata
                if ( this.isNew() ) {
                    // This is a new upload, use MN.create()
                return MetacatUI.appModel.get("objectServiceUrl") +
                        (encodeURIComponent(this.get("id")));

                }
                else {
                    if ( this.hasUpdates() ) {
                        if ( this.hasContentChanges ) {
                            // Exists on the server, use MN.update()
                        return MetacatUI.appModel.get("objectServiceUrl") +
                            (encodeURIComponent(this.get("id")));

                        } else {
                            // Exists on the server, use MN.updateSystemMetadata()
                        return MetacatUI.appModel.get("metaServiceUrl") +
                                (encodeURIComponent(this.get("id")));

                        }

                    } else {
                      // Use MN.getSystemMetadata()
                      return MetacatUI.appModel.get("metaServiceUrl") +
                              (encodeURIComponent(this.get("id")) ||
                               encodeURIComponent(this.get("seriesid")));
                    }

                }
              },

              /*
               * Overload Backbone.Model.fetch, so that we can set custom options for each fetch() request
               */
              fetch: function(options){

                if ( ! options ) var options = {};
                else var options = _.clone(options);

                // Default to GET /meta
                options.url = MetacatUI.appModel.get("metaServiceUrl") + encodeURIComponent(this.get("id"));

                //If we are using the Solr service to retrieve info about this object, then construct a query
                if((typeof options != "undefined") && options.solrService){

                  //Get basic information
                  var query = "";

                  //Do not search for seriesId when it is not configured in this model/app
                  if(typeof this.get("seriesid") === "undefined")
                    query += 'id:"' + encodeURIComponent(this.get("id")) + '"';
                  //If there is no seriesid set, then search for pid or sid
                  else if(!this.get("seriesid"))
                    query += '(id:"' + encodeURIComponent(this.get("id")) + '" OR seriesId:"' + encodeURIComponent(this.get("id")) + '")';
                  //If a seriesId is specified, then search for that
                  else if(this.get("seriesid") && (this.get("id").length > 0))
                    query += '(seriesId:"' + encodeURIComponent(this.get("seriesid")) + '" AND id:"' + encodeURIComponent(this.get("id")) + '")';
                  //If only a seriesId is specified, then just search for the most recent version
                  else if(this.get("seriesid") && !this.get("id"))
                    query += 'seriesId:"' + encodeURIComponent(this.get("id")) + '" -obsoletedBy:*';

                  //The fields to return
                  var fl = "formatId,formatType,documents,isDocumentedBy,id,seriesId";

                  //Use the Solr query URL
                  var solrOptions = {
                    url: MetacatUI.appModel.get("queryServiceUrl") + 'q=' + query + "&fl=" + fl + "&wt=json"
                  }

                  //Merge with the options passed to this function
                  var fetchOptions = _.extend(options, solrOptions);
                }
                else if(typeof options != "undefined"){
                  //Use custom options for retreiving XML
                  //Merge with the options passed to this function
                  var fetchOptions = _.extend({
                    dataType: "text"
                  }, options);
                }
                else{
                  //Use custom options for retreiving XML
                  var fetchOptions = _.extend({
                    dataType: "text"
                  });
                }

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

                //Call Backbone.Model.fetch to retrieve the info
                return Backbone.Model.prototype.fetch.call(this, fetchOptions);

              },

              /*
               * This function is called by Backbone.Model.fetch.
               * It deserializes the incoming XML from the /meta REST endpoint and converts it into JSON.
               */
              parse: function(response){

                // If the response is XML
                if( (typeof response == "string") && response.indexOf("<") == 0 ) {

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

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

                  //Find the XML node for the system metadata
                  for(var i=0; i<responseDoc.length; i++){
                    if((responseDoc[i].nodeType == 1) && (responseDoc[i].localName.indexOf("systemmetadata") > -1)){
                      systemMetadata = responseDoc[i];
                      break;
                    }
                  }

                  //Parse the XML to JSON
                  var sysMetaValues = this.toJson(systemMetadata);

                  //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){
                    var camelCasedKey = this.nodeNameMap()[key];
                    if(camelCasedKey){
                      sysMetaValues[camelCasedKey] = sysMetaValues[key];
                      delete sysMetaValues[key];
                    }
                  }, this);

                  //Save the checksum from the system metadata in a separate attribute on the model
                  sysMetaValues.originalChecksum = sysMetaValues.checksum;
                  sysMetaValues.checksum = this.defaults().checksum;

                  //Save the identifier as the id attribute
                  sysMetaValues.id = sysMetaValues.identifier;

                  //Parse the Access Policy
                  if( this.get("accessPolicy") && AccessPolicy.prototype.isPrototypeOf(this.get("accessPolicy")) ){
                    this.get("accessPolicy").parse($(systemMetadata).find("accesspolicy"));
                    sysMetaValues.accessPolicy = this.get("accessPolicy");
                  }
                  else{
                    //Create a new AccessPolicy collection, if there isn't one already.
                    sysMetaValues.accessPolicy = this.createAccessPolicy($(systemMetadata).find("accesspolicy"));
                  }

                  return sysMetaValues;

                // If the response is a list of Solr docs
                } else if (( typeof response === "object") && (response.response && response.response.docs)){

                  //If no objects were found in the index, mark as notFound and exit
                  if(!response.response.docs.length){
                    this.set("notFound", true);
                    this.trigger("notFound");
                    return;
                  }

                  //Get the Solr document (there should be only one)
                  var doc = response.response.docs[0];

                  //Take out any empty values
                  _.each(Object.keys(doc), function(field){
                    if( !doc[field] && doc[field] !== 0 )
                      delete doc[field];
                  });

                  return doc;
                }
                else
                    // Default to returning the raw response
                    return response;
              },

              // A utility function for converting XML to JSON
              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 == "false" ? false : item.nodeValue == "true" ? true : 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;
              },

              /*
              Serialize the DataONE object JSON to XML
              Parameters:
                json - the JSON object to convert to XML
                containerNode - an HTML element to inser the resulting XML into
             */
             toXML: function(json, containerNode){

                if(typeof json == "string"){
                  containerNode.textContent = json;
                  return containerNode;
                }

                for(var i=0; i<Object.keys(json).length; i++){
                  var key = Object.keys(json)[i],
                    contents = json[key] || json[key];

                  var node = document.createElement(key);

                  //Skip this attribute if it is not populated
                  if(!contents || (Array.isArray(contents) && !contents.length))
                    continue;

                  //If it's a simple text node
                  if(typeof contents == "string"){
                    containerNode.textContent = contents;
                    return containerNode;
                  }
                  else if(Array.isArray(contents)){
                    var allNewNodes = [];

                    for(var ii=0; ii<contents.length; ii++){
                      allNewNodes.push(this.toXML(contents[ii], $(node).clone()[0]));
                    }

                    if(allNewNodes.length)
                      node = allNewNodes;
                  }
                  else if(typeof contents == "object"){
                    $(node).append(this.toXML(contents, node));
                    var attributeNames = _.without(Object.keys(json[key]), "content");
                  }

                  $(containerNode).append(node);
                }

                return containerNode;
             },

              /*
              * Saves the DataONEObject System Metadata to the server
              */
              save: function(attributes, options){

                  // Set missing file names before saving
                  if ( ! this.get("fileName") ) {
                      this.setMissingFileName();
                  }
                  else{
                    //Replace all non-alphanumeric characters with underscores
                    var fileNameWithoutExt = this.get("fileName").substring(0, this.get("fileName").lastIndexOf(".")),
                        extension = this.get("fileName").substring(this.get("fileName").lastIndexOf("."), this.get("fileName").length);
                    this.set("fileName", fileNameWithoutExt.replace(/[^a-zA-Z0-9]/g, "_") + extension);
                  }

                  if ( !this.hasUpdates() ) {
                      this.set("uploadStatus", null);
                      return;
                  }

                  //Set the upload transfer as in progress
                  this.set("uploadProgress", 2);
                  this.set("uploadStatus", "p");

                  //Check if the checksum has been calculated yet.
                  if( !this.get("checksum") ){
                    //When it is calculated, restart this function
                    this.on("checksumCalculated", this.save);
                    //Calculate the checksum for this file
                    this.calculateChecksum();
                    //Exit this function until the checksum is done
                    return;
                  }

                  //Create a FormData object to send data with our XHR
                  var formData = new FormData();

                  //If this is not a new object, update the id. New DataONEObjects will have an id
                  // created during initialize.
                  if( !this.isNew() ){
                    this.updateID();
                    formData.append("pid", this.get("oldPid"));
                    formData.append("newPid", this.get("id"));
                  }
                  else{
                    //Create an ID if there isn't one
                    if( !this.get("id") ){
                      this.set("id", "urn:uuid:" + uuid.v4());
                    }

                    //Add the identifier to the XHR data
                    formData.append("pid", this.get("id"));
                  }

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

                  //Send the system metadata as a Blob
                  var xmlBlob = new Blob([sysMetaXML], {type : 'application/xml'});
                  //Add the system metadata XML to the XHR data
                  formData.append("sysmeta", xmlBlob, "sysmeta.xml");

                  // Create the new object (MN.create())
                  formData.append("object", this.get("uploadFile"), this.get("fileName"));

                  var model = this;

                  // On create(), add to the package and the metadata
                  // Note: This should be added to the parent collection
                  // but for now we are using the root collection
                  _.each(this.get("collections"), function(collection){

                    if(collection.type == "DataPackage"){
                      this.off("successSaving", collection.addNewModel);
                      this.once("successSaving", collection.addNewModel, collection);
                    }

                  }, this);

                  //Put together the AJAX and Backbone.save() options
                  var requestSettings = {
                      url: this.url(),
                      cache: false,
                      contentType: false,
                      dataType: "text",
                      processData: false,
                      data: formData,
                      parse: false,
                      xhr: function(){
                        var xhr = new window.XMLHttpRequest();

                        //Upload progress
                        xhr.upload.addEventListener("progress", function(evt){
                          if (evt.lengthComputable) {
                            var percentComplete = evt.loaded / evt.total * 100;

                            model.set("uploadProgress", percentComplete);
                          }
                        }, false);

                        return xhr;
                      },
                      success: function(model, response, xhr){

                          model.set("numSaveAttempts", 0);
                          model.set("uploadStatus", "c");
                          model.trigger("successSaving", model);

                          // Get the newest sysmeta set by the MN
                          model.fetch({
                            merge: true,
                            systemMetadataOnly: true
                          });

                          // Reset the content changes status
                          model.set("hasContentChanges", false);

                          //Reset the model isNew attribute
                          model.set("isNew", false);

                          //Set the last-calculated checksum as the original checksum
                          model.set("originalChecksum", model.get("checksum"));
                          model.set("checksum", model.defaults().checksum);
                      },
                      error: function(model, response, xhr){

                        //Reset the identifier changes
                        model.resetID();
                        //Reset the checksum, if this is a model that needs to be serialized with each save.
                        if( model.serialize ){
                          model.set("checksum", model.defaults().checksum);
                        }

                        model.set("numSaveAttempts", model.get("numSaveAttempts") + 1);
                        var numSaveAttempts = model.get("numSaveAttempts");

                        if( numSaveAttempts < 3 && (response.status == 408 || response.status == 0) ){

                            //Try saving again in 10, 40, and 90 seconds
                            setTimeout(function(){
                                model.save.call(model);
                              },
                              (numSaveAttempts * numSaveAttempts) * 10000);
                        }
                        else{
                          model.set("numSaveAttempts", 0);

                          var parsedResponse = $(response.responseText).not("style, title").text();

                          //When there is no network connection (status == 0), there will be no response text
                          if(!parsedResponse)
                            parsedResponse = "There was a network issue that prevented this file from uploading. " +
                                     "Make sure you are connected to a reliable internet connection.";

                          model.set("errorMessage", parsedResponse);

                          //Set the model status as e for error
                          model.set("uploadStatus", "e");

                          //Trigger a custom event for the model save error
                          model.trigger("errorSaving", parsedResponse);

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

                  //Add the user settings
                  requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings());

                  //Send the Save request
                  Backbone.Model.prototype.save.call(this, null, requestSettings);
            },

            /**
              * Updates the DataONEObject System Metadata to the server
              */
            updateSysMeta: function () {

              //Update the upload status to "p" for "in progress"
              this.set("uploadStatus", "p");
              //Update the system metadata upload status to "p" as well, so the app
              // knows that the system metadata, specifically, is being updated.
              this.set("sysMetaUploadStatus", "p");

              var formData = new FormData();

              //Add the identifier to the XHR data
              formData.append("pid", this.get("id"));

              var sysMetaXML = this.serializeSysMeta();

              //Send the system metadata as a Blob
              var xmlBlob = new Blob([sysMetaXML], {type: 'application/xml'});
              //Add the system metadata XML to the XHR data
              formData.append("sysmeta", xmlBlob, "sysmeta.xml");

              var model = this;

              var requestSettings = {
                  url: MetacatUI.appModel.get("metaServiceUrl") + (encodeURIComponent(this.get("id"))),
                  cache: false,
                  contentType: false,
                  dataType: "text",
                  type: "PUT",
                  processData: false,
                  data: formData,
                  parse: false,
                  success: function() {

                    model.set("numSaveAttempts", 0);

                    //Fetch the system metadata from the server so we have a fresh copy of the newest sys meta.
                    model.fetch({ systemMetadataOnly: true });

                    //Update the upload status to "c" for "complete"
                    model.set("uploadStatus", "c");
                    model.set("sysMetaUploadStatus", "c");

                    //Trigger a custom event that the sys meta was updated
                    model.trigger("sysMetaUpdated");
                  },
                  error: function (xhr, status, statusCode) {

                    model.set("numSaveAttempts", model.get("numSaveAttempts") + 1);
                    var numSaveAttempts = model.get("numSaveAttempts");

                    if (numSaveAttempts < 3 && (statusCode == 408 || statusCode == 0)) {

                        //Try saving again in 10, 40, and 90 seconds
                        setTimeout(function () {
                                model.updateSysMeta.call(model);
                            },
                            (numSaveAttempts * numSaveAttempts) * 10000);
                    } else {
                          model.set("numSaveAttempts", 0);

                          var parsedResponse = $(xhr.responseText).not("style, title").text();

                          //When there is no network connection (status == 0), there will be no response text
                          if (!parsedResponse)
                              parsedResponse = "There was a network issue that prevented this file from updating. " +
                                  "Make sure you are connected to a reliable internet connection.";

                          model.set("errorMessage", parsedResponse);
                          model.set("uploadStatus", "e");
                          model.set("sysMetaUploadStatus", "e");

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

              //Add the user settings
              requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings());

              //Send the XHR
              $.ajax(requestSettings);
            },

            /**
             * Check if the current user is authorized to perform an action on this object
             * @param {string} action - The action (read, write, or changePermission) to check
             * if the current user has authorization to perform. This function doesn't return
             * the result of the check, but it sends an XHR, updates this model, and triggers a change event.
             */
            checkAuthority: function(action, options){

              // return false - if neither PID nor SID is present to check the authority
              if ( (this.get("id") == null)  && (this.get("seriesId") == null) ) {
                return false;
              }

              if( typeof options == "undefined" ){
                var options = {};
              }

              // If PID is not present - check authority with seriesId
              var identifier = this.get("id");
              if ( (identifier == null) ) {
                identifier = this.get("seriesId");
              }

              if(!action) var action = "changePermission";

              var authServiceUrl = MetacatUI.appModel.get('authServiceUrl');
              if(!authServiceUrl)
                return false;

              var onSuccess = options.onSuccess || function(data, textStatus, xhr) {
                    model.set("isAuthorized", true);
                    model.trigger("change:isAuthorized");
                  },
                  onError = options.onError || function(xhr, textStatus, errorThrown){
                    if(errorThrown == 404){
                      model.set("notFound", true);
                      model.trigger("notFound");
                    }
                    else{
                      model.set("isAuthorized", false);
                    }
                  };

              var model = this;
              var requestSettings = {
                url: authServiceUrl + encodeURIComponent(identifier) + "?action=" + action,
                type: "GET",
                success: onSuccess,
                error: onError
              }
              $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));

            },

            serializeSysMeta: function(){
                //Get the system metadata XML that currently exists in the system
                var sysMetaXML = this.get("sysMetaXML"), // sysmeta as string
                    xml, // sysmeta as DOM object
                    accessPolicyXML, // The generated access policy XML
                    previousSiblingNode, // A DOM node indicating any previous sibling
                    rightsHolderNode, // A DOM node for the rights holder field
                    accessPolicyNode, // A DOM node for the access policy
                    replicationPolicyNode, // A DOM node for the replication policy
                    obsoletesNode, // A DOM node for the obsoletes field
                    obsoletedByNode, // A DOM node for the obsoletedBy field
                    fileNameNode, // A DOM node for the file name
                    xmlString, // The system metadata document as a string
                    nodeNameMap, // The map of camelCase to lowercase attributes
                    extension; // the file name extension for this object

                if ( typeof sysMetaXML === "undefined" || sysMetaXML === null ) {
                    xml = this.createSysMeta();
                } else {
                    xml = $($.parseHTML(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("submitter").text(this.get("submitter") || MetacatUI.appUserModel.get("username"));
                xml.find("formatid").text(this.get("formatId") || this.getFormatId());

                //If there is a seriesId, add it
                if( this.get("seriesId") ){
                  //Get the seriesId XML node
                  var seriesIdNode = xml.find("seriesId");

                  //If it doesn't exist, create one
                  if( !seriesIdNode.length ){
                    seriesIdNode = $(document.createElement("seriesid"));
                    xml.find("identifier").before(seriesIdNode);
                  }

                  //Add the seriesId string to the XML node
                  seriesIdNode.text( this.get("seriesId") );
                }

                //If there is no size, get it
                if( !this.get("size") && this.get("uploadFile")){
                  this.set("size", this.get("uploadFile").size);
                }

                //Get the size of the file, if there is one
                if( this.get("uploadFile") ){
                  xml.find("size").text( this.get("uploadFile").size );
                }
                //Otherwise, use the last known size
                else{
                  xml.find("size").text(this.get("size"));
                }

                //Update the checksum and checksum algorithm
                if( !this.get("checksum") && this.get("originalChecksum") ){
                  xml.find("checksum").text(this.get("originalChecksum"));
                }
                else{
                  xml.find("checksum").text(this.get("checksum"));
                }

                xml.find("checksum").attr("algorithm", this.get("checksumAlgorithm"));

                //Update the rightsholder
                xml.find("rightsholder").text(this.get("rightsHolder") || MetacatUI.appUserModel.get("username"));

                //Write the access policy
                accessPolicyXML = this.get("accessPolicy").serialize();

                // Get the access policy node, if it exists
                accessPolicyNode = xml.find("accesspolicy");

                previousSiblingNode = xml.find("rightsholder");

                // Create an access policy node if needed
                if ( (! accessPolicyNode.length) && accessPolicyXML ) {
                    accessPolicyNode = $(document.createElement("accesspolicy"));
                    previousSiblingNode.after(accessPolicyNode);

                }

                //Replace the old access policy with the new one if it exists
                if ( accessPolicyXML ) {
                  accessPolicyNode.replaceWith(accessPolicyXML);
                }

                  // Set the obsoletes node after replPolicy or accessPolicy, or rightsHolder
                  replicationPolicyNode = xml.find("replicationpolicy");
                  accessPolicyNode = xml.find("accesspolicy");
                  rightsHolderNode = xml.find("rightsholder");

                  if ( replicationPolicyNode.length ) {
                      previousSiblingNode = replicationPolicyNode;
                  } else if ( accessPolicyNode.length ) {
                      previousSiblingNode = accessPolicyNode;
                  } else {
                      previousSiblingNode = rightsHolderNode;
                  }

                  obsoletesNode = xml.find("obsoletes");

                  if( this.get("obsoletes") ){
                       if( obsoletesNode.length ) {
                         obsoletesNode.text(this.get("obsoletes"));
                       }
                       else {
                         obsoletesNode = $(document.createElement("obsoletes")).text(this.get("obsoletes"));
                         previousSiblingNode.after(obsoletesNode);
                       }
                  }
                  else {
                    if ( obsoletesNode ) {
                      obsoletesNode.remove();
                    }
                  }

                    if ( obsoletesNode ) {
                        previousSiblingNode = obsoletesNode;
                    }

                    obsoletedByNode = xml.find("obsoletedby");

                    //remove the obsoletedBy node if it exists
                    // TODO: Verify this is what we want to do
                    if ( obsoletedByNode ) {
                        obsoletedByNode.remove();
                    }

                    xml.find("archived").text(this.get("archived") || "false");
                    xml.find("dateuploaded").text(this.get("dateUploaded") || new Date().toISOString());

                    //Get the filename node
                    fileNameNode = xml.find("filename");

                    //If the filename node doesn't exist, then create one
                    if( ! fileNameNode.length ){
                      fileNameNode = $(document.createElement("filename"));
                      xml.find("dateuploaded").after(fileNameNode);
                    }

                    //Set the object file name
                    $(fileNameNode).text(this.get("fileName"));

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

                    //Now camel case the nodes
                    nodeNameMap = this.nodeNameMap();

                    _.each(Object.keys(nodeNameMap), function(name, i){
                      var originalXMLString = xmlString;

                      //Camel case node names
                      var regEx = new RegExp("<" + name, "g");
                      xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name]);
                      var regEx = new RegExp(name + ">", "g");
                      xmlString = xmlString.replace(regEx, nodeNameMap[name] + ">");

                      //If node names haven't been changed, then find an attribute
                      if(xmlString == originalXMLString){
                        var regEx = new RegExp(" " + name + "=", "g");
                        xmlString = xmlString.replace(regEx, " " + nodeNameMap[name] + "=");
                      }
                    }, this);

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

                    return xmlString;
              },

              /*
               * Get the object format identifier for this object
               */
              getFormatId: function() {

                  var formatId = "application/octet-stream", // default to untyped data
                  objectFormats = [],  // The list of potential format matches
                  ext; // The extension of the filename for this object

                  if ( MetacatUI.objectFormats.length > 0 ) {

                      // Does the media type match the object format id?
                      objectFormats = MetacatUI.objectFormats.where({formatId: this.get("mediaType")});

                      if ( objectFormats.length > 0 ) {
                          formatId = objectFormats[0].get("formatId");
                          objectFormats = [];
                          return formatId;

                      }

                      // Does the media type match the media type name?
                      objectFormats = MetacatUI.objectFormats.where({mediaType: {_name: this.get("mediaType")}});

                      if ( objectFormats.length > 0 ) {
                          formatId = objectFormats[0].get("formatId");
                          objectFormats = [];
                          return formatId;

                      }

                      // Does the extension match the extension?
                      // TODO: multiple formats have the same extension - need to discern them, but how?
                      if ( typeof this.get("fileName") !== "undefined" &&
                         this.get("fileName") !== null && this.get("fileName").length > 1 ) {

                          ext = this.get("fileName").substring(
                                      this.get("fileName").lastIndexOf(".") + 1,
                                      this.get("fileName").length);
                          objectFormats = MetacatUI.objectFormats.where({extension: ext});

                          if ( objectFormats.length > 0 ) {

                            //If this is a "nc" file, assume it is a netCDF-3 file.
                            if( ext == "nc" ){
                              formatId = "netCDF-3";
                            }
                            else{
                                  formatId = objectFormats[0].get("formatId");
                                  objectFormats = [];
                            }

                              return formatId;

                          }
                      }
                  }

                  return formatId;
              },

              /*
               * Build a fresh system metadata document for this object when it is new
               * Return it as a DOM object
               */
              createSysMeta: function() {
                  var sysmetaDOM, // The DOM
                      sysmetaXML = []; // The document as a string array

                  sysmetaXML.push(
                      //'<?xml version="1.0" encoding="UTF-8"?>',
                      '<d1_v2.0:systemmetadata',
                      '    xmlns:d1_v2.0="http://ns.dataone.org/service/types/v2.0"',
                      '    xmlns:d1="http://ns.dataone.org/service/types/v1">',
                      '    <serialversion />',
                      '    <identifier />',
                      '    <formatid />',
                      '    <size />',
                      '    <checksum />',
                      '    <submitter />',
                      '    <rightsholder />',
                      '    <filename />',
                      '</d1_v2.0:systemmetadata>'
                  );

                  sysmetaDOM = $($.parseHTML(sysmetaXML.join("")));
                  return sysmetaDOM;
              },

              /*
              * Create an access policy for this DataONEObject using the default access
              * policy set in the AppModel.
              *
              * @param {DOM Element} [accessPolicyXML] - An <accessPolicy> XML node
              *   that contains a list of access rules.
              * @return {AccessPolicy} - an AccessPolicy collection that represents the
              *   given XML or the default policy set in the AppModel.
              */
              createAccessPolicy: function(accessPolicyXML){
                //Create a new AccessPolicy collection
                var accessPolicy = new AccessPolicy();

                accessPolicy.dataONEObject = this;

                //If there is no access policy XML sent,
                if( !accessPolicyXML ){
                  //Set the default access policy using the AppModel configuration
                  accessPolicy.createDefaultPolicy();
                }
                else{
                  //Parse the access policy XML to create AccessRule models from the XML
                  accessPolicy.parse(accessPolicyXML);
                }

                //Listen to changes on the collection and trigger a change on this model
                var self = this;
                this.listenTo(accessPolicy, "change update", function(){
                  self.trigger("change");
                  this.addToUploadQueue();

                });

                return accessPolicy;
              },

              updateID: function(id){
                //Save the attributes so we can reset the ID later
                this.attributeCache = this.toJSON();

                //Set the old identifier
                var oldPid = this.get("id"),
                    selfDocuments,
                    selfDocumentedBy,
                    documentedModels,
                    documentedModel,
                    index;

                //Save the current id as the old pid
                this.set("oldPid", oldPid);

                //Create a new seriesId, if there isn't one, and if this model specifies that one is required
                if( !this.get("seriesId") && this.get("createSeriesId") ){
                  this.set("seriesId", "urn:uuid:" + uuid.v4());
                }

                // Check to see if the old pid documents or is documented by itself
                selfDocuments = _.contains(this.get("documents"), oldPid);
                selfDocumentedBy = _.contains(this.get("isDocumentedBy"), oldPid);

                //Set the new identifier
                if( id ) {
                  this.set("id", id);

                } else {
                  if( this.get("type") == "DataPackage" ){
                    this.set("id", "resource_map_urn:uuid:" + uuid.v4());
                  }
                  else{
                    this.set("id", "urn:uuid:" + uuid.v4());
                  }
                }

                // Remove the old pid from the documents list if present
                if ( selfDocuments ) {
                    index = this.get("documents").indexOf(oldPid);
                    if ( index > -1 ) {
                        this.get("documents").splice(index, 1);

                    }
                    // And add the new pid in
                    this.get("documents").push(this.get("id"));

                }

                // Remove the old pid from the isDocumentedBy list if present
                if ( selfDocumentedBy ) {
                    index = this.get("isDocumentedBy").indexOf(oldPid);
                    if ( index > -1 ) {
                        this.get("isDocumentedBy").splice(index, 1);

                    }
                    // And add the new pid in
                    this.get("isDocumentedBy").push(this.get("id"));

                }

                // Update all models documented by this pid with the new id
                _.each(this.get("documents"), function(id) {
                    documentedModels = MetacatUI.rootDataPackage.where({id: id}),
                    documentedModel;

                    if ( documentedModels.length > 0 ) {
                        documentedModel = documentedModels[0];
                    }
                    if ( typeof documentedModel !== "undefined" ) {
                        // Find the oldPid in the array
                        if( Array.isArray(documentedModel.get("isDocumentedBy")) ){
                          index = documentedModel.get("isDocumentedBy").indexOf("oldPid");

                          if ( index > -1 ) {
                              // Remove it
                              documentedModel.get("isDocumentedBy").splice(index, 1);

                          }
                          // And add the new pid in
                          documentedModel.get("isDocumentedBy").push(this.get("id"));
                        }
                    }
                }, this);

                this.trigger("change:id")

                //Update the obsoletes and obsoletedBy
                this.set("obsoletes", oldPid);
                this.set("obsoletedBy", null);

                // Update the latest version of this object
                this.set("latestVersion", this.get("id"));

                //Set the archived option to false
                this.set("archived", false);
              },

              /*
               * Reset
               */
              resetID: function(){
                if(!this.attributeCache) return false;

                this.set("oldPid", this.attributeCache.oldPid, {silent:true});
                this.set("id", this.attributeCache.id, {silent: true});
                this.set("obsoletes", this.attributeCache.obsoletes, {silent: true});
                this.set("obsoletedBy", this.attributeCache.obsoletedBy, {silent: true});
                this.set("archived", this.attributeCache.archived, {silent: true});
                this.set("latestVersion", this.attributeCache.latestVersion, {silent: true});

                //Reset the attribute cache
                this.attributeCache = {};
              },

              /*
               * Checks if this model has updates that need to be synced with the server.
               */
              hasUpdates: function(){
                if(this.isNew()) return true;

                //Compare the new system metadata XML to the old system metadata XML
                var newSysMeta = this.serializeSysMeta(),
                  oldSysMeta = $(document.createElement("div")).append($(this.get("sysMetaXML"))).html();

                    if ( oldSysMeta === "" ) return false;

                return !(newSysMeta == oldSysMeta);
              },

              /*
                 Set the changed flag on any system metadata or content attribute changes,
                 and set the hasContentChanges flag on content changes only
               */
              handleChange: function(model, options) {
                if(!model) var model = this;

                  var sysMetaAttrs = ["serialVersion", "identifier", "formatId", "formatType", "size", "checksum",
                      "checksumAlgorithm", "submitter", "rightsHolder", "accessPolicy", "replicationAllowed",
                      "replicationPolicy", "obsoletes", "obsoletedBy", "archived", "dateUploaded", "dateSysMetadataModified",
                      "originMemberNode", "authoritativeMemberNode", "replica", "seriesId", "mediaType", "fileName"],
                      nonSysMetaNonContentAttrs = _.difference(model.get("originalAttrs"), sysMetaAttrs),
                      allChangedAttrs = Object.keys(model.changedAttributes()),
                      changedSysMetaOrContentAttrs = [], //sysmeta or content attributes that have changed
                      changedContentAttrs = []; // attributes from sub classes like ScienceMetadata or EML211 ...

                  // Get a list of all changed sysmeta and content attributes
                  changedSysMetaOrContentAttrs = _.difference(allChangedAttrs, nonSysMetaNonContentAttrs);
                  if ( changedSysMetaOrContentAttrs.length > 0 ) {
                      // For any sysmeta or content change, set the package dirty flag
                      if ( MetacatUI.rootDataPackage &&
                           MetacatUI.rootDataPackage.packageModel &&
                           ! MetacatUI.rootDataPackage.packageModel.get("changed") &&
                           model.get("synced") ) {

                          MetacatUI.rootDataPackage.packageModel.set("changed", true);
                      }
                  }

                  // And get a list of all changed content attributes
                  changedContentAttrs = _.difference(changedSysMetaOrContentAttrs, sysMetaAttrs);

                  if ( changedContentAttrs.length > 0 && !this.get("hasContentChanges") && model.get("synced") ) {
                    this.set("hasContentChanges", true);
                    this.addToUploadQueue();
                  }

                },

              /**
              * Returns true if this DataONE object is new. A DataONE object is new
              * if there is no upload date and it's been synced (i.e. been fetched)
              * @return {boolean}
              */
              isNew: function(){
                //Check if there is an upload date that was retrieved from the server
                return ( this.get("dateUploaded") === this.defaults().dateUploaded &&
                         this.get("synced") );
              },

              /**
               * Updates the upload status attribute on this model and marks the collection as changed
               */
              addToUploadQueue: function(){

                if( !this.get("synced") ){
                  return;
                }

                  //Add this item to the queue
                  if((this.get("uploadStatus") == "c") || (this.get("uploadStatus") == "e") || !this.get("uploadStatus")){
                    this.set("uploadStatus", "q");

                    //Mark each DataPackage collection this model is in as changed
                    _.each(this.get("collections"), function(collection){
                      if(collection.packageModel)
                        collection.packageModel.set("changed", true);
                    }, this);
                  }
              },

              /*
               * Updates the progress percentage when the model is getting uploaded
               */
              updateProgress: function(e){
                if(e.lengthComputable){
                    var max = e.total;
                    var current = e.loaded;

                    var Percentage = (current * 100)/max;

                    if(Percentage >= 100)
                    {
                       // process completed
                    }
                  }
              },

              /*
               * Updates the relationships with other models when this model has been updated
               */
              updateRelationships: function(){
                _.each(this.get("collections"), function(collection){
                  //Get the old id for this model
                  var oldId = this.get("oldPid");

                  if(!oldId) return;

                  //Find references to the old id in the documents relationship
                  var  outdatedModels = collection.filter(function(m){
                      return _.contains(m.get("documents"), oldId);
                    });

                  //Update the documents array in each model
                  _.each(outdatedModels, function(model){
                    var updatedDocuments = _.without(model.get("documents"), oldId);
                    updatedDocuments.push(this.get("id"));

                    model.set("documents", updatedDocuments);
                  }, this);

                }, this);
              },

              /*
               * Finds the latest version of this object by travesing the obsolescence chain
               */
              findLatestVersion: function(latestVersion, possiblyNewer){
                // Make sure we have the /meta service configured
                if(! MetacatUI.appModel.get('metaServiceUrl')) return;

                //If there is no system metadata, then retrieve it first
                if(!this.get("sysMetaXML")){
                  this.once("sync", this.findLatestVersion);
                  this.once("systemMetadataSync", this.findLatestVersion);
                  this.fetch({
                    url: MetacatUI.appModel.get("metaServiceUrl") + encodeURIComponent(this.get("id")),
                    dataType: "text",
                    systemMetadataOnly: true
                  });
                  return;
                }

                //If no pid was supplied, use this model's id
                if(!latestVersion || typeof latestVersion != "string"){
                  var latestVersion = this.get("id");
                  var possiblyNewer = this.get("obsoletedBy");
                }

                //If this isn't obsoleted by anything, then there is no newer version
                if(!possiblyNewer || typeof latestVersion != "string"){
                  this.set("latestVersion", latestVersion);

                  //Trigger an event that will fire whether or not the latestVersion
                  // attribute was actually changed
                  this.trigger("latestVersionFound", this);

                  //Remove the listeners now that we found the latest version
                  this.stopListening("sync", this.findLatestVersion);
                  this.stopListening("systemMetadataSync", this.findLatestVersion);

                  return;
                }

                var model = this;

                //Get the system metadata for the possibly newer version
                var requestSettings = {
                  url: MetacatUI.appModel.get('metaServiceUrl') + encodeURIComponent(possiblyNewer),
                  type: "GET",
                  success: function(data) {

                    // the response may have an obsoletedBy element
                    var obsoletedBy = $(data).find("obsoletedBy").text();

                    //If there is an even newer version, then get it and rerun this function
                    if(obsoletedBy){
                      model.findLatestVersion(possiblyNewer, obsoletedBy);
                    }
                    //If there isn't a newer version, then this is it
                    else{
                      model.set("latestVersion", possiblyNewer);
                      model.trigger("latestVersionFound", model);

                      //Remove the listeners now that we found the latest version
                      model.stopListening("sync", model.findLatestVersion);
                      model.stopListening("systemMetadataSync", model.findLatestVersion);
                    }

                  },
                  error: function(xhr){
                    //If this newer version isn't accessible, link to the latest version that is
                    if(xhr.status == "401"){
                      model.set("latestVersion", latestVersion);
                      model.trigger("latestVersionFound", model);
                    }
                  }
                }

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

              /*
               * Will format an XML string or XML nodes by camel-casing the node names, as necessary
               */
              formatXML: function(xml){
                var nodeNameMap = this.nodeNameMap(),
                    xmlString = "";

                //XML must be provided for this function
                if(!xml)
                  return "";
                //Support XML strings
                else if(typeof xml == "string")
                  xmlString = xml;
                //Support DOMs
                else if(typeof xml == "object" && xml.nodeType){
                  //XML comments should be formatted with start and end carets
                  if(xml.nodeType == 8)
                    xmlString = "<" + xml.nodeValue + ">";
                  //XML nodes have the entire XML string available in the outerHTML attribute
                  else if(xml.nodeType == 1)
                    xmlString = xml.outerHTML;
                  //Text node types are left as-is
                  else if(xml.nodeType == 3)
                    return xml.nodeValue;
                }

                //Return empty strings if something went wrong
                if(!xmlString)
                  return "";

                _.each(Object.keys(nodeNameMap), function(name, i){
                  var originalXMLString = xmlString;

                  //Check for this node name whe it's an opening XML node, e.g. `<name>`
                  var regEx = new RegExp("<" + name + ">", "g");
                  xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + ">");

                  //Check for this node name when it's an opening XML node, e.g. `<name `
                  regEx = new RegExp("<" + name + " ", "g");
                  xmlString = xmlString.replace(regEx, "<" + nodeNameMap[name] + " ");

                  //Check for this node name when it's preceeded by a namespace, e.g. `:name `
                  regEx = new RegExp(":" + name + " ", "g");
                  xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + " ");

                  //Check for this node name when it's a closing tag preceeded by a namespace, e.g. `:name>`
                  regEx = new RegExp(":" + name + ">", "g");
                  xmlString = xmlString.replace(regEx, ":" + nodeNameMap[name] + ">");

                  //Check for this node name when it's a closing XML tag, e.g. `</name>`
                  regEx = new RegExp("</" + name + ">", "g");
                  xmlString = xmlString.replace(regEx, "</" + nodeNameMap[name] + ">");

                  //If node names haven't been changed, then find an attribute, e.g. ` name=`
                  if(xmlString == originalXMLString){
                    regEx = new RegExp(" " + name + "=", "g");
                    xmlString = xmlString.replace(regEx, " " + nodeNameMap[name] + "=");
                  }

                }, this);

                //Take each XML node text value and decode any XML entities
                var regEx = new RegExp("\&[0-9a-zA-Z]+\;", "g");
                xmlString = xmlString.replace(regEx, function(match){ return he.encode(he.decode(match)); });

                return xmlString;
              },

              /**
               * Convert number of bytes into human readable format
               *
               * @return sizeStr for the given model
               */
              bytesToSize: function(){
                  var kilobyte = 1024;
                  var megabyte = kilobyte * 1024;
                  var gigabyte = megabyte * 1024;
                  var terabyte = gigabyte * 1024;
                  var precision = 0;

                  var bytes = this.get("size");

                  if ((bytes >= 0) && (bytes < kilobyte)) {
                      this.set("sizeStr", bytes + ' B');

                  } else if ((bytes >= kilobyte) && (bytes < megabyte)) {
                      this.set("sizeStr", (bytes / kilobyte).toFixed(precision) + ' KB');

                  } else if ((bytes >= megabyte) && (bytes < gigabyte)) {
                      precision = 2;
                      this.set("sizeStr", (bytes / megabyte).toFixed(precision) + ' MB');

                  } else if ((bytes >= gigabyte) && (bytes < terabyte)) {
                      precision = 2;
                      this.set("sizeStr", (bytes / gigabyte).toFixed(precision) + ' GB');

                  } else if (bytes >= terabyte) {
                      precision = 2;
                      this.set("sizeStr", (bytes / terabyte).toFixed(precision) + ' TB');

                  } else {
                      this.set("sizeStr", bytes + ' B');

                  }
              },

              /* Ensure we have a file name */
              setMissingFileName: function() {
                var objectFormats, filename, extension;

                objectFormats = MetacatUI.objectFormats.where({formatId: this.get("formatId")});
                if ( objectFormats.length > 0 ) {
                    extension = objectFormats[0].get("extension");
                }

                //Science metadata file names will use the title
                if( this.get("type") == "Metadata" ){
                  filename = (Array.isArray(this.get("title")) && this.get("title").length)? this.get("title")[0] : this.get("id");
                }
                //Resource maps will use a "resource_map_" prefix
                else if( this.get("type") == "DataPackage" ){
                  filename = "resource_map_" + this.get("id");
                  extension = ".rdf.xml";
                }
                //All other object types will just use the id
                else{
                  filename = this.get("id");
                }

                //Replace all non-alphanumeric characters with underscores
                filename = filename.replace(/[^a-zA-Z0-9]/g, "_");

                if ( typeof extension !== "undefined" ) {
                  filename = filename + "." + extension;
                }

                this.set("fileName", filename);
              },

              /**
              * Creates a URL for viewing more information about this object
              * @return {string}
              */
              createViewURL: function(){
                return MetacatUI.root + "/view/" + encodeURIComponent((this.get("seriesId") || this.get("id")));
              },

              /**
              * Converts the identifier string to a string safe to use in an XML id attribute
              * @param {string} [id] - The ID string
              * @return {string} - The XML-safe string
              */
              getXMLSafeID: function(id){

                if(typeof id == "undefined"){
                  var id = this.get("id");
                }

                //Replace XML id attribute invalid characters and patterns in the identifier
                id = id.replace(/</g, "-").replace(/:/g, "-").replace(/&[a-zA-Z0-9]+;/g);

                return id;
              },

              /**** Provenance-related functions ****/
              /*
               * Returns true if this provenance field points to a source of this data or metadata object
               */
              isSourceField: function(field){
                if((typeof field == "undefined") || !field) return false;
                // Is the field we are checking a prov field?
                if(!_.contains(MetacatUI.appSearchModel.getProvFields(), field)) return false;

                if(field == "prov_generatedByExecution" ||
                   field == "prov_generatedByProgram"   ||
                   field == "prov_used"           ||
                   field == "prov_wasDerivedFrom"     ||
                   field == "prov_wasInformedBy")
                  return true;
                else
                  return false;
               },

              /*
               * Returns true if this provenance field points to a derivation of this data or metadata object
               */
              isDerivationField: function(field){
                if((typeof field == "undefined") || !field) return false;
                if(!_.contains(MetacatUI.appSearchModel.getProvFields(), field)) return false;

                if(field == "prov_usedByExecution" ||
                   field == "prov_usedByProgram"   ||
                   field == "prov_hasDerivations" ||
                   field == "prov_generated")
                  return true;
                else
                  return false;
              },

              //Returns a plain-english version of the general format - either image, program, metadata, PDF, annotation or data
              getType: function(){
                //The list of formatIds that are images

                        //The list of formatIds that are images
                var pdfIds = ["application/pdf"];
                var annotationIds = ["http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html"];

                // Type has already been set, use that.
                if(this.get("type").toLowerCase() == "metadata")
                  return "metadata";

                //Determine the type via provONE
                var instanceOfClass = this.get("prov_instanceOfClass");
                if(typeof instanceOfClass !== "undefined" && Array.isArray(instanceOfClass) && instanceOfClass.length){
                  var programClass = _.filter(instanceOfClass, function(className){
                    return (className.indexOf("#Program") > -1);
                  });
                  if((typeof programClass !== "undefined") && programClass.length)
                    return "program";
                }
                else{
                  if(this.get("prov_generated").length || this.get("prov_used").length)
                    return "program";
                }

                //Determine the type via file format
                if(this.isSoftware()) return "program";
                if(this.isData()) return "data";

                if(this.get("type").toLowerCase() == "metadata") return "metadata";
                        if(this.isImage()) return "image";
                if(_.contains(pdfIds, this.get("formatId")))   return "PDF";
                if(_.contains(annotationIds, this.get("formatId")))   return "annotation";

                else return "data";
              },

              /*
               * param dataObject - a SolrResult representing the data object returned from the index
               * returns - true if this data object is an image, false if it is other
               */
              isImage: function(){
                //The list of formatIds that are images
                var imageIds = ["image/gif",
                                "image/jp2",
                                "image/jpeg",
                                "image/png"];

                //Does this data object match one of these IDs?
                if(_.indexOf(imageIds, this.get('formatId')) == -1) return false;
                else return true;

              },

              isData: function() {
                var dataIds =  ["application/atom+xml",
                                "application/mathematica",
                                "application/msword",
                                "application/netcdf",
                                "application/octet-stream",
                                "application/pdf",
                                "application/postscript",
                                "application/rdf+xml",
                                "application/rtf",
                                "application/vnd.google-earth.kml+xml",
                                "application/vnd.ms-excel",
                                "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
                                "application/vnd.ms-powerpoint",
                                "application/vnd.openxmlformats-officedocument.presentationml.presentation",
                                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                                "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                                "application/x-bzip2",
                                "application/x-fasta",
                                "application/x-gzip",
                                "application/x-rar-compressed",
                                "application/x-tar",
                                "application/xhtml+xml",
                                "application/xml",
                                "application/zip",
                                "audio/mpeg",
                                "audio/x-ms-wma",
                                "audio/x-wav",
                                        "image/svg xml",
                                        "image/svg+xml",
                                        "image/bmp",
                                "image/tiff",
                                "text/anvl",
                                "text/csv",
                                "text/html",
                                "text/n3",
                                "text/plain",
                                "text/tab-separated-values",
                                "text/turtle",
                                "text/xml",
                                "video/avi",
                                "video/mp4",
                                "video/mpeg",
                                "video/quicktime",
                                "video/x-ms-wmv"];

                //Does this data object match one of these IDs?
                if(_.indexOf(dataIds, this.get('formatId')) == -1) return false;
                else return true;
              },

              isSoftware: function(){
                //The list of formatIds that are programs
                var softwareIds =  ["text/x-python",
                          "text/x-rsrc",
                          "text/x-matlab",
                          "text/x-sas",
                          "application/R",
                          "application/x-ipynb+json"];
                //Does this data object match one of these IDs?
                if(_.indexOf(softwareIds, this.get('formatId')) == -1) return false;
                else return true;
              },

              /*
               * param dataObject - a SolrResult representing the data object returned from the index
               * returns - true if this data object is a pdf, false if it is other
               */
              isPDF: function(){
                //The list of formatIds that are images
                var ids = ["application/pdf"];

                //Does this data object match one of these IDs?
                if(_.indexOf(ids, this.get('formatId')) == -1) return false;
                else return true;
              },

              /*
               * Set the DataONE ProvONE provenance class
               * param className - the shortened form of the actual classname value. The
               * shortname will be appened to the ProvONE namespace, for example,
               * the className "program" will result in the final class name
               * "http://purl.dataone.org/provone/2015/01/15/ontology#Program"
               * see https://github.com/DataONEorg/sem-prov-ontologies/blob/master/provenance/ProvONE/v1/provone.html
               */
               setProvClass: function(className) {
                 className = className.toLowerCase();
                 className = className.charAt(0).toUpperCase() + className.slice(1)

                 /* This function is intended to be used for the ProvONE classes that are
                  * typically represented in DataONEObjects: "Data", "Program", and hopefully
                  * someday "Execution", as we don't allow the user to set the namespace
                  * e.g. to "PROV", so therefor we check for the currently known ProvONE classes.
                  */
                  if (_.contains(['Program', 'Data', 'Visualization', 'Document', 'Execution', 'User'], className)) {
                      this.set("prov_instanceOfClass", [this.PROVONE + className]);
                  } else if (_.contains(['Entity', 'Usage', 'Generation', 'Association'], className)) {
                      this.set("prov_instanceOfClass", [this.PROV + className]);
                  } else {
                     message = "The given class name: " + className + " is not in the known ProvONE or PROV classes."
                     throw new Error(message);
                  }
               },

              /*
               *  Calculate a checksum for the object
               *  @param algorithm  The algorithm to use, defaults to MD5
               *  @return checksum  A checksum plain JS object with value and algorithm attributes
               */
              calculateChecksum: function(algorithm) {
                var algorithm = algorithm || "MD5";
                var checksum = {algorithm: undefined, value: undefined};
                var hash; // The checksum hash
                var file; // The file to be read by slicing
                var reader; // The FileReader used to read each slice
                var offset = 0; // Byte offset for reading slices
                var sliceSize = Math.pow(2,20) // 1MB slices
                var model = this;

                // Do we have a file?
                if (this.get("uploadFile") instanceof Blob) {
                    file = this.get("uploadFile");
                    reader = new FileReader();
                    /* Handle load errors */
                    reader.onerror = function(event) {
                        console.log("Error reading: " + event);
                    };
                    /* Show progress */
                    reader.onprogress = function(event) {
                    };
                    /* Handle load finish */
                    reader.onloadend = function(event) {
                        if (event.target.readyState == FileReader.DONE) {
                            hash.update(event.target.result);
                        }
                        offset += sliceSize;
                        if ( _seek() ) {
                            model.set("checksum", hash.hex());
                            model.set("checksumAlgorithm", checksum.algorithm);
                            model.trigger("checksumCalculated", model.attributes);
                        };
                    };
                } else {
                    message = "The given object is not a blob or a file object."
                    throw new Error(message);
                }

                switch ( algorithm ) {
                    case "MD5":
                        checksum.algorithm = algorithm;
                        hash = md5.create();
                        _seek();
                        break;
                    case "SHA-1":
                        // TODO: Support SHA-1
                        // break;
                    default:
                        message = "The given algorithm: " + algorithm + " is not supported."
                        throw new Error(message);
                }

                /*
                 *  A helper function internal to calculateChecksum() used to slice
                 *  the file at the next offset by slice size
                 */
                function _seek() {
                    var calculated = false;
                    var slice;
                    // Digest the checksum when we're done calculating
                    if (offset >= file.size) {
                        hash.digest();
                        calculated = true;
                        return calculated;
                    }
                    // slice the file and read the slice
                    slice = file.slice(offset, offset + sliceSize);
                    reader.readAsArrayBuffer(slice);
                    return calculated;

                }
            },

            /**
        		 * Checks if the pid or sid or given string is a DOI
        		 *
        		 * @param {string} customString - Optional. An identifier string to check instead of the id and seriesId attributes on the model
        		 * @returns {boolean} True if it is a DOI
        		 */
        		isDOI: function(customString) {
        			var DOI_PREFIXES = ["doi:10.", "http://dx.doi.org/10.", "http://doi.org/10.", "http://doi.org/doi:10.",
        				"https://dx.doi.org/10.", "https://doi.org/10.", "https://doi.org/doi:10."],
        				  DOI_REGEX = new RegExp(/^10.\d{4,9}\/[-._;()/:A-Z0-9]+$/i);;

        			//If a custom string is given, then check that instead of the seriesId and id from the model
        			if( typeof customString == "string" ){
        				for (var i=0; i < DOI_PREFIXES.length; i++) {
        					if (customString.toLowerCase().indexOf(DOI_PREFIXES[i].toLowerCase()) == 0 )
        						return true;
        				}

        				//If there is no DOI prefix, check for a DOI without the prefix using a regular expression
        				if( DOI_REGEX.test(customString) ){
        					return true;
        				}

        			}
        			else{
        				var seriesId = this.get("seriesId"),
        						pid      = this.get("id");

        				for (var i=0; i < DOI_PREFIXES.length; i++) {
        					if (seriesId && seriesId.toLowerCase().indexOf(DOI_PREFIXES[i].toLowerCase()) == 0 )
        						return true;
        					else if (pid && pid.toLowerCase().indexOf(DOI_PREFIXES[i].toLowerCase()) == 0 )
        						return true;
        				}

        				//If there is no DOI prefix, check for a DOI without the prefix using a regular expression
        				if( DOI_REGEX.test(seriesId) || DOI_REGEX.test(pid) ){
        					return true;
        				}

        			}

        			return false;
        		}
        },
        {
          /* Generate a unique identifier to be used as an XML id attribute */
          generateId: function() {
            var idStr = ''; // the id to return
            var length = 30; // the length of the generated string
            var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'.split('');

            for (var i = 0; i < length; i++) {
              idStr += chars[Math.floor(Math.random() * chars.length)];
            }
            return idStr;
          }
        });

        return DataONEObject;
    });