/*global define */ define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) { // SolrResult Model // ------------------ var SolrResult = Backbone.Model.extend({ // This model contains all of the attributes found in the SOLR 'docs' field inside of the SOLR response element defaults: { abstract: null, entityName: null, indexed: true, archived: false, origin: '', title: '', pubDate: '', id: '', seriesId: null, resourceMap: null, downloads: null, citations: 0, selected: false, formatId: null, formatType: null, fileName: null, datasource: null, rightsHolder: null, size: 0, type: null, url: null, obsoletedBy: null, geohash_9: null, read_count_i: 0, reads: 0, isDocumentedBy: null, isPublic: null, isService: false, serviceDescription: null, serviceTitle: null, serviceEndpoint: null, serviceOutput: null, notFound: false, newestVersion: null, provSources: [], provDerivations: [], //Provenance index fields prov_generated: null, prov_generatedByDataONEDN: null, prov_generatedByExecution: null, prov_generatedByFoafName: null, prov_generatedByOrcid: null, prov_generatedByProgram: null, prov_generatedByUser: null, prov_hasDerivations: null, prov_hasSources: null, prov_used: null, prov_usedByDataONEDN: null, prov_usedByExecution: null, prov_usedByFoafName: null, prov_usedByOrcid: null, prov_usedByProgram: null, prov_usedByUser: null, prov_wasDerivedFrom: null, prov_wasExecutedByExecution: null, prov_wasExecutedByUser: null, prov_wasGeneratedBy: null, prov_wasInformedBy: null }, initialize: function(){ this.setURL(); this.on("change:id", this.setURL); this.set("type", this.getType()); this.on("change:read_count_i", function(){ this.set("reads", this.get("read_count_i"))}); }, type: "SolrResult", // Toggle the `selected` state of the result toggle: function () { this.selected = !this.get('selected'); }, //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 var imageIds = ["image/gif", "image/jp2", "image/jpeg", "image/png", "image/svg xml", "image/svg+xml", "image/bmp"]; //The list of formatIds that are images var pdfIds = ["application/pdf"]; var annotationIds = ["http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html"]; //Determine the type via provONE var instanceOfClass = this.get("prov_instanceOfClass"); if(typeof instanceOfClass !== "undefined"){ 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") || this.get("prov_used")) return "program"; } //Determine the type via file format if(this.get("formatType") == "METADATA") return "metadata"; if(_.contains(imageIds, this.get("formatId"))) return "image"; if(_.contains(pdfIds, this.get("formatId"))) return "PDF"; if(_.contains(annotationIds, this.get("formatId"))) return "annotation"; else return "data"; }, //Returns a plain-english version of the specific format ID (for selected ids) getFormat: function(){ var formatMap = { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : "Microsoft Excel OpenXML", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "Microsoft Word OpenXML", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" : "Microsoft Office Excel 2007 binary workbooks", "application/vnd.openxmlformats-officedocument.presentationml.presentation" : "Microsoft Office OpenXML Presentation", "application/vnd.ms-excel" : "Microsoft Excel", "application/msword" : "Microsoft Word", "application/vnd.ms-powerpoint" : "Microsoft Powerpoint", "text/html" : "HTML", "text/plain": "plain text (.txt)", "video/avi" : "Microsoft AVI file", "video/x-ms-wmv" : "Windows Media Video (.wmv)", "audio/x-ms-wma" : "Windows Media Audio (.wma)", "application/vnd.google-earth.kml xml" : "Google Earth Keyhole Markup Language (KML)", "http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html" : "annotation", "application/mathematica" : "Mathematica Notebook", "application/postscript" : "Postscript", "application/rtf" : "Rich Text Format (RTF)", "application/xml" : "XML Application", "text/xml" : "XML", "application/x-fasta" : "FASTA sequence file", "nexus/1997" : "NEXUS File Format for Systematic Information", "anvl/erc-v02" : "Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13", "http://purl.org/dryad/terms/" : "Dryad Metadata Application Profile Version 3.0", "http://datadryad.org/profile/v3.1" : "Dryad Metadata Application Profile Version 3.1", "application/pdf" : "PDF", "application/zip" : "ZIP file", "http://www.w3.org/TR/rdf-syntax-grammar" : "RDF/XML", "http://www.w3.org/TR/rdfa-syntax" : "RDFa", "application/rdf xml" : "RDF", "text/turtle" : "TURTLE", "text/n3" : "N3", "application/x-gzip" : "GZIP Format", "application/x-python" : "Python script", "http://www.w3.org/2005/Atom" : "ATOM-1.0", "application/octet-stream" : "octet stream (application file)", "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd" : "Darwin Core, v2.0", "http://rs.tdwg.org/dwc/xsd/simpledarwincore/" : "Simple Darwin Core", "eml://ecoinformatics.org/eml-2.1.0" : "EML v2.1.0", "eml://ecoinformatics.org/eml-2.1.1" : "EML v2.1.1", "eml://ecoinformatics.org/eml-2.0.1" : "EML v2.0.1", "eml://ecoinformatics.org/eml-2.0.0" : "EML v2.0.0" } return formatMap[this.get("formatId")] || this.get("formatId"); }, setURL: function(){ if(appModel.get("objectServiceUrl")) this.set("url", appModel.get("objectServiceUrl") + encodeURIComponent(this.get("id"))); else if(appModel.get("resolveServiceUrl")) this.set("url", appModel.get("resolveServiceUrl") + encodeURIComponent(this.get("id"))); }, // checks if the pid is already 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."]; //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; } } 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; } return false; }, /* * Checks if the currently-logged-in user is authorized to change permissions on this doc */ checkAuthority: function(){ var authServiceUrl = appModel.get('authServiceUrl'); if(!authServiceUrl) return false; var model = this; var requestSettings = { url: authServiceUrl + encodeURIComponent(this.get("id")) + "?action=changePermission", 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, appUserModel.createAjaxSettings())); }, /* * This method will download this object while sending the user's auth token in the request. */ downloadWithCredentials: function(){ //if(this.get("isPublic")) return; //Get info about this object var url = this.get("url"), model = this; //Create an XHR var xhr = new XMLHttpRequest(); if(appUserModel.get("loggedIn")) 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 to save this file as var filename = xhr.getResponseHeader('Content-Disposition'); if(!filename){ filename = model.get("fileName") || model.get("title") || model.get("id") || ""; } else filename = filename.substring(filename.indexOf("filename=")+9).replace(/"/g, ""); filename = filename.trim(); //For IE, we need to use the navigator API if (navigator && navigator.msSaveOrOpenBlob) { navigator.msSaveOrOpenBlob(xhr.response, filename); } //Other browsers can download it via a link else{ var a = document.createElement('a'); a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob // Set the file name. a.download = filename a.style.display = 'none'; document.body.appendChild(a); a.click(); delete a; } //Trigger the download complete event model.trigger("downloadComplete"); }; xhr.onerror = function(e){ var a = document.createElement('a'); a.href = url; var filename = model.get("fileName") || model.get("title") || model.get("id") || ""; if(filename) a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); model.trigger("downloadComplete"); }; xhr.onprogress = function(e){ if (e.lengthComputable){ var percent = (e.loaded / e.total) * 100; model.set("downloadPercent", percent); } }; //Open and send the request with the user's auth token xhr.open('GET', url); xhr.responseType = "blob"; if(appUserModel.get("loggedIn")) xhr.setRequestHeader("Authorization", "Bearer " + appUserModel.get("token")); xhr.send(); }, getInfo: function(fields){ var model = this; if(!fields) var fields = "id,seriesId,fileName,resourceMap,formatType,formatId,obsoletedBy,isDocumentedBy,documents,title,origin,pubDate,dateUploaded,datasource,replicaMN,isAuthorized,isPublic,size,read_count_i,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription,serviceType"; var escapeSpecialChar = appSearchModel.escapeSpecialChar; var query = "q="; //Do not search for seriesId when it is not configured in this model/app if(!appModel.get("useSeriesId")) query += 'id:"' + escapeSpecialChar(encodeURIComponent(this.get("id"))) + '"'; //If there is no seriesId set, then search for pid or sid else if(!this.get("seriesId")) query += '(id:"' + escapeSpecialChar(encodeURIComponent(this.get("id"))) + '" OR seriesId:"' + escapeSpecialChar(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:"' + escapeSpecialChar(encodeURIComponent(this.get("seriesId"))) + '" AND id:"' + escapeSpecialChar(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:"' + escapeSpecialChar(encodeURIComponent(this.get("id"))) + '" -obsoletedBy:*'; var requestSettings = { url: appModel.get("queryServiceUrl") + query + '&fl='+fields+'&wt=json', type: "GET", success: function(data, response, xhr){ var docs = data.response.docs; if(docs.length == 1){ model.set(docs[0]); model.trigger("sync"); } //If we searched by seriesId, then let's find the most recent version in the series else if(docs.length > 1){ var mostRecent = _.reject(docs, function(doc){ return (typeof doc.obsoletedBy !== "undefined"); }); if(mostRecent.length > 0) model.set(mostRecent[0]); else model.set(docs[0]); //Just default to the first doc found model.trigger("sync"); } else{ model.set("indexed", false); //Try getting the system metadata as a backup model.getSysMeta(); } }, error: function(xhr, textStatus, errorThrown){ model.set("indexed", false); model.trigger("getInfoError"); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, getCitationInfo: function(){ this.getInfo("id,seriesId,origin,pubDate,dateUploaded,title,datasource"); }, /* * Get the system metadata for this object */ getSysMeta: function(){ var url = appModel.get("metaServiceUrl") + this.get("id"), model = this; var requestSettings = { url: url, type: "GET", dataType: "text", success: function(data, response, xhr){ //Check if this is archvied var archived = ($(data).find("archived").text() == "true"); model.set("archived", archived); //Get the file size model.set("size", ($(data).find("size").text() || "")); //Get the entity name model.set("filename", ($(data).find("filename").text() || "")); //Check if this is a metadata doc var formatId = $(data).find("formatid").text() || "", formatType; model.set("formatId", formatId); if((formatId.indexOf("ecoinformatics.org") > -1) || (formatId.indexOf("FGDC") > -1) || (formatId.indexOf("INCITS") > -1) || (formatId.indexOf("namespaces/netcdf") > -1) || (formatId.indexOf("waterML") > -1) || (formatId.indexOf("darwin") > -1) || (formatId.indexOf("dryad") > -1) || (formatId.indexOf("http://www.loc.gov/METS") > -1) || (formatId.indexOf("ddi:codebook:2_5") > -1) || (formatId.indexOf("http://www.icpsr.umich.edu/DDI") > -1) || (formatId.indexOf("http://purl.org/ornl/schema/mercury/terms/v1.0") > -1) || (formatId.indexOf("datacite") > -1) || (formatId.indexOf("isotc211") > -1) || (formatId.indexOf("metadata") > -1)) model.set("formatType", "METADATA"); //Trigger the sync event so the app knows we found the model info model.trigger("sync"); }, error: function(){ model.notFound(); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, notFound: function(){ this.set({"notFound": true}, {silent: true}); this.trigger("404"); }, //Transgresses the obsolence chain until it finds the newest version that this user is authorized to read findLatestVersion: function(newestVersion, possiblyNewer) { // Make sure we have the /meta service configured if(!appModel.get('metaServiceUrl')) return; //If no pid was supplied, use this model's id if(!newestVersion){ var newestVersion = this.get("id"); var possiblyNewer = this.get("obsoletedBy"); } //If this isn't obsoleted by anything, then there is no newer version if(!possiblyNewer){ this.set("newestVersion", newestVersion); return; } var model = this; //Get the system metadata for the possibly newer version var requestSettings = { url: 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("newestVersion", possiblyNewer); }, error: function(xhr){ //If this newer version isn't accessible, link to the latest version that is if(xhr.status == "401") model.set("newestVersion", newestVersion); } } $.ajax(_.extend(requestSettings, appUserModel.createAjaxSettings())); }, /**** 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; if(!_.contains(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(appSearchModel.getProvFields(), field)) return false; if(field == "prov_usedByExecution" || field == "prov_usedByProgram" || field == "prov_generated" || field == "prov_hasDerivations") return true; else return false; }, /* * Returns true if this SolrResult has a provenance trace (i.e. has either sources or derivations) */ hasProvTrace: function(){ if(!appModel.get("prov")) return false; if(this.get("formatType") == "METADATA"){ if(this.get("prov_hasSources") || this.get("prov_hasDerivations")) return true; } var fieldNames = appSearchModel.getProvFields(), currentField = ""; for(var i=0; i < fieldNames.length; i++){ currentField = fieldNames[i]; if(this.has(currentField)) return true; } return false; }, /* * Returns an array of all the IDs of objects that are sources of this object */ getSources: function(){ var sources = new Array(), model = this, //Get the prov fields but leave out references to executions which are not used in the UI yet fields = _.reject(appSearchModel.getProvFields(), function(f){ return f.indexOf("xecution") > -1 }); //Leave out the first e in execution so we don't have to worry about case sensitivity _.each(fields, function(provField, i){ if(model.isSourceField(provField) && model.has(provField)) sources.push(model.get(provField)); }); return _.uniq(_.flatten(sources)); }, /* * Returns an array of all the IDs of objects that are derivations of this object */ getDerivations: function(){ var derivations = new Array(), model = this, //Get the prov fields but leave out references to executions which are not used in the UI yet fields = _.reject(appSearchModel.getProvFields(), function(f){ return f.indexOf("xecution") > -1 }); //Leave out the first e in execution so we don't have to worry about case sensitivity _.each(fields, function(provField, i){ if(model.isDerivationField(provField) && model.get(provField)) derivations.push(model.get(provField)); }); return _.uniq(_.flatten(derivations)); }, getInputs: function(){ return this.get("prov_used"); }, getOutputs: function(){ return this.get("prov_generated"); }, /****************************/ /** * 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 SolrResult; });