{
	"translatorID":"14763d25-8ba0-45df-8f52-b8d1108e7ac9",
	"translatorType":3,
	"label":"Bibliontology RDF",
	"creator":"Simon Kornblith",
	"target":"rdf",
	"minVersion":"2.0",
	"maxVersion":"",
	"priority":50,
	"browserSupport":"gcs",
	"configOptions":{"getCollections":"true", "dataMode":"rdf/xml"},
	"displayOptions":{"exportNotes":true, "exportFileData":false},
	"inRepository":false,
	"lastUpdated":"2011-07-08 04:50:39"
}

var n = {
	address:"http://schemas.talis.com/2005/address/schema#",	// could also use vcard?
	bibo:"http://purl.org/ontology/bibo/",
	ctag:"http://commontag.org/ns#",
	dcterms:"http://purl.org/dc/terms/",
	doap:"http://usefulinc.com/ns/doap#",
	foaf:"http://xmlns.com/foaf/0.1/",
	link:"http://purl.org/rss/1.0/modules/link/",
	po:"http://purl.org/ontology/po/",
	rdf:"http://www.w3.org/1999/02/22-rdf-syntax-ns#",
	rel:"http://www.loc.gov/loc.terms/relators/",
	res:"http://purl.org/vocab/resourcelist/schema#",
	sc:"http://umbel.org/umbel/sc/",
	sioct:"http://rdfs.org/sioc/types#",
	z:"http://www.zotero.org/namespaces/export#"
};

/**
   Types should be in the form
   
   <ZOTERO_TYPE>: [<ITEM_CLASS>, <SUBCONTAINER_CLASS>, <CONTAINER_CLASS>]
   
   Item classes should be in the form
   
   [[<PREDICATE>, <OBJECT>]+]
   
   This generates the triples
   
   (ITEM		<PREDICATE>				<OBJECT>)+
   
   Subcontainer and container classes should be in the form
   
   [<ALWAYS_INCLUDE>, <ITEM_PREDICATE>, [<CONTAINER_PREDICATE>, <CONTAINER_OBJECT>]*] | null
   
   If there is a property to be applied to the container, or if <ALWAYS_INCLUDE> is true, then this
   generates
   
   ITEM			<ITEM_PREDICATE>		CONTAINER
   (CONTAINER	<CONTAINER_PREDICATE>	<CONTAINER_OBJECT>)*
 **/
 
//	ZOTERO TYPE				ITEM CLASS											SUBCONTAINER CLASS					CONTAINER CLASS
var TYPES = {
	"artwork":				[[[n.rdf+"type", n.bibo+"Image"]], 					null, 								null],
	"attachment":			[[[n.rdf+"type", n.z+"Attachment"]], 				null, 								null],
	"audioRecording":		[[[n.rdf+"type", n.bibo+"AudioDocument"]],			null,								null],
	"bill":					[[[n.rdf+"type", n.bibo+"Bill"]],					null,								[false, n.dcterms+"isPartOf", [[n.rdf+"type", n.bibo+"Code"]]]],					
	"blogPost":				[[[n.rdf+"type", n.sioct+"BlogPost"],
							  [n.rdf+"type", n.bibo+"Article"]],				null,								[false, n.dcterms+"isPartOf", [[n.rdf+"type", n.sioct+"Weblog"],
							   																						[n.rdf+"type", n.bibo+"Website"]]]],
	"book":					[[[n.rdf+"type", n.bibo+"Book"]],					null,								null],
	"bookSection":			[[[n.rdf+"type", n.bibo+"BookSection"]],			null,								[false, n.dcterms+"isPartOf", [[n.rdf+"type", n.bibo+"EditedBook"]]]],
	"case":					[[[n.rdf+"type", n.bibo+"LegalCaseDocument"]],		null,								[false, n.dcterms+"isPartOf", [[n.rdf+"type", n.bibo+"CourtReporter"]]]],
	"computerProgram":		[[[n.rdf+"type", n.sc+"ComputerProgram_CW"],
							  [n.rdf+"type", n.bibo+"Document"]], 				null,								null],
	"conferencePaper":		[[[n.rdf+"type", n.bibo+"Article"]],				null,								[true, n.dcterms+"isPartOf", [[n.rdf+"type", n.bibo+"Proceedings"]]]],
	"dictionaryEntry":		[[[n.rdf+"type", n.bibo+"Article"]], 				null,								[true, n.dcterms+"isPartOf", [[n.rdf+"type", n.sc+"Dictionary"],
																													[n.rdf+"type", n.bibo+"ReferenceSource"]]]],
	"document":				[[[n.rdf+"type", n.bibo+"Document"]],				null,								null],
	"email":				[[[n.rdf+"type", n.bibo+"Email"]],					null,								null],
	"encyclopediaArticle":	[[[n.rdf+"type", n.bibo+"Article"]], 				null,								[true, n.dcterms+"isPartOf", [[n.rdf+"type", n.sc+"Encyclopedia"],
																													[n.rdf+"type", n.bibo+"ReferenceSource"]]]],
	"forumPost":			[[[n.rdf+"type", n.sioct+"BoardPost"],
							  [n.rdf+"type", n.bibo+"Article"]],				null,								[false, n.dcterms+"isPartOf", [[n.rdf+"type", n.sioct+"MessageBoard"],
							 																					 	[n.rdf+"type", n.bibo+"Website"]]]],
	"film":					[[[n.rdf+"type", n.bibo+"Film"]],					null,								null],
	"hearing":				[[[n.rdf+"type", n.bibo+"Hearing"]],				null,								null],
	"instantMessage":		[[[n.rdf+"type", n.sioct+"InstantMessage"],
							  [n.rdf+"type", n.bibo+"PersonalCommunication"]], 	null,								null],
	"interview":			[[[n.rdf+"type", n.bibo+"Interview"]],				null,								null],
	"journalArticle":		[[[n.rdf+"type", n.bibo+"AcademicArticle"]], 		[true, n.dcterms+"isPartOf",
																				[[n.rdf+"type", n.bibo+"Issue"]]], 	[true, n.dcterms+"isPartOf", [[n.rdf+"type", n.bibo+"Journal"]]]],
	"letter":				[[[n.rdf+"type", n.bibo+"Letter"]],					null,								null],
	"magazineArticle":		[[[n.rdf+"type", n.bibo+"Article"]], 				[true, n.dcterms+"isPartOf",
																				[[n.rdf+"type", n.bibo+"Issue"]]], 	[true, n.dcterms+"isPartOf", [[n.rdf+"type", n.bibo+"Magazine"]]]],
	"manuscript":			[[[n.rdf+"type", n.bibo+"Manuscript"]],				null,								null],
	"map":					[[[n.rdf+"type", n.bibo+"Map"]],					null,								null],
	"newspaperArticle":		[[[n.rdf+"type", n.bibo+"Article"]], 				[true, n.dcterms+"isPartOf",
																				[[n.rdf+"type", n.bibo+"Issue"]]], 	[true, n.dcterms+"isPartOf", [[n.rdf+"type", n.bibo+"Newspaper"]]]],
	"note":					[[[n.rdf+"type", n.bibo+"Note"]],					null,								null],
	"patent":				[[[n.rdf+"type", n.bibo+"Patent"]],					null,								null],
	"podcast":				[[[n.rdf+"type", n.z+"Podcast"],
							  [n.rdf+"type", n.bibo+"AudioDocument"]],			null,								null],
	"presentation":			[[[n.rdf+"type", n.bibo+"Slideshow"]],				null,								null],
	"radioBroadcast":		[[[n.rdf+"type", n.po+"AudioDocument"],
							  [n.rdf+"type", n.po+"Episode"],
							  [n.po+"broadcast_on", n.po+"Radio"]],				null,								[false, n.dcterms+"isPartOf", [[n.rdf+"type", n.po+"Programme"]]]],
	"report":				[[[n.rdf+"type", n.bibo+"Report"]],					null,								null],
	"statute":				[[[n.rdf+"type", n.bibo+"Statute"]],				null,								[false, n.dcterms+"isPartOf", [[n.rdf+"type", n.bibo+"Code"]]]],
	"thesis":				[[[n.rdf+"type", n.bibo+"Thesis"]],					null,								null],
	"tvBroadcast":			[[[n.rdf+"type", n.bibo+"AudioVisualDocument"],
							  [n.rdf+"type", n.po+"Episode"],
							  [n.po+"broadcast_on", n.po+"TV"]],				null,								[false, n.dcterms+"isPartOf", [[n.rdf+"type", n.po+"Programme"]]]],
	"videoRecording":		[[[n.rdf+"type", n.bibo+"AudioVisualDocument"]], 	null,								null],
	"webpage":				[[[n.rdf+"type", n.bibo+"Webpage"]],				null,								[false, n.dcterms+"isPartOf", [[n.rdf+"type", n.bibo+"Website"]]]]
};

/**
 * This is just a map of un-namespaced BIBO item types to Zotero item types
 */
var BIBO_TYPES = {
	"Article":							"magazineArticle",
	"Brief":							"case",
	"Chapter":							"bookSection",
	"CollectedDocument":				"document",
	"DocumentPart":						"document",
	"EditedBook":						"book",
	"Excerpt":							"note",
	"Quote":							"note",
	"Film":								"videoRecording",
	"LegalDecision":					"case",
	"LegalDocument":					"case",
	"Legislation":						"bill",
	"Manual":							"book",
	"Performance":						"presentation",
	"PersonalCommunication":			"letter",
	"PersonalCommunicationDocument":	"letter",
	"Slide":							"presentation",
	"Standard":							"report",
	"Website":							"webpage"
};

var USERITEM = 1;
var ITEM = 2;
var SUBCONTAINER = 3;
var CONTAINER = 4;
var ITEM_SERIES = 5;
var SUBCONTAINER_SERIES = 6; 	// not used
var CONTAINER_SERIES = 7;

/**
    Fields should be in the form
    
    <ZOTERO_FIELD>: ([<SUBJECT>, <PREDICATE>] | <FUNCTION>)
    
    If a <FUNCTION> is specified, then it is passed the item and should return a set of triples in
    the form
    
    [[<SUBJECT>, <PREDICATE>, <OBJECT>, <LITERAL>]*]
    
    where <SUBJECT> refers to one of the constants defined above. If <LITERAL> is true, then
    <OBJECT> is treated as a literal.
    
    If a <FUNCTION> is not used and <PREDICATE> is a string, then the parameters generate a triple
    in the form
    
    <SUBJECT> 		<PREDICATE>				FIELD_CONTENT
    
    where <SUBJECT> refers to one of the constants defined above. Alternatively, <PREDICATE> may be
    an array in the form
    
	[<ITEM_PREDICATE>, [<BLANK_NODE_PREDICATE>, <BLANK_NODE_OBJECT>]*, <PREDICATE>]
	
	This generates the triples
	
	<SUBJECT>		<ITEM_PREDICATE>		<BLANK_NODE>
	(<BLANK_NODE>	<BLANK_NODE_PREDICATE>	<BLANK_NODE_OBJECT>)*
	<BLANK_NODE>	<PREDICATE>				FIELD_CONTENT
**/
var FIELDS = {
	"url":					[ITEM, 			n.bibo+"uri"],
	"rights":				[USERITEM,		n.dcterms+"rights"],
	"series":				[CONTAINER_SERIES,	n.dcterms+"title"],
	"volume":				[SUBCONTAINER,	n.bibo+"volume"],
	"issue":				[SUBCONTAINER,	n.bibo+"issue"],
	"edition":				[SUBCONTAINER,	n.bibo+"edition"],
	"place":				[CONTAINER,		[n.dcterms+"publisher", [[n.rdf+"type", n.foaf+"Organization"]], n.address+"localityName"]],
	"country":				[CONTAINER,		[n.dcterms+"publisher", [[n.rdf+"type", n.foaf+"Organization"]], n.address+"countryName"]],
	"publisher":			[CONTAINER,		[n.dcterms+"publisher", [[n.rdf+"type", n.foaf+"Organization"]], n.foaf+"name"]],
	"pages":				[ITEM,			n.bibo+"pages"],
	"firstPage":			[ITEM,			n.bibo+"pageStart"],
	"ISBN":					[function(item) {
		var isbns = item.ISBN.split(/, ?| /g);
		var triples = [];
		for each(var isbn in isbns) {
			if(isbn.length == 10) {
				triples.push([CONTAINER, n.bibo+"isbn10", isbn, true]);
			} else {
				triples.push([CONTAINER, n.bibo+"isbn13", isbn, true]);
			}
		}
		return triples;
	}, function(nodes) {
		var isbns = [];
		for each(var prop in [n.bibo+"isbn13", n.bibo+"isbn10"]) {
			var statements = Zotero.RDF.getStatementsMatching(nodes[CONTAINER], prop, null);
			if(statements) {
				for each(var statement in statements) {
					isbns.push(statement[2]);
				}
			}
		}
		if(!isbns.length) return false;
		return isbns.join(", ");
	}],
	"publicationTitle":		[CONTAINER,		n.dcterms+"title"],
	"ISSN":					[CONTAINER,		n.bibo+"issn"],
	"date":					[SUBCONTAINER,	n.dcterms+"date"],
	"section":				[ITEM,			n.bibo+"section"],
	"callNumber":			[SUBCONTAINER,	n.bibo+"lccn"],
	"archiveLocation":		[ITEM,			n.dcterms+"source"],
	"distributor":			[SUBCONTAINER,	n.bibo+"distributor"],
	"extra":				[ITEM,			n.z+"extra"],
	"journalAbbreviation":	[CONTAINER,		n.bibo+"shortTitle"],
	"DOI":					[ITEM,			n.bibo+"doi"],
	"accessDate":			[USERITEM,		n.z+"accessDate"],
	"seriesTitle":			[ITEM_SERIES,	n.dcterms+"title"],
	"seriesText":			[ITEM_SERIES,	n.dcterms+"description"],
	"seriesNumber":			[CONTAINER_SERIES,		n.bibo+"number"],
	"code":					[CONTAINER,		n.dcterms+"title"],
	"session":				[ITEM,			[n.bibo+"presentedAt", [[n.rdf+"type", n.bibo+"Conference"]], n.dcterms+"title"]],
	"legislativeBody":		[ITEM,			[n.bibo+"organizer", [[n.rdf+"type", n.sc+"LegalGovernmentOrganization"], [n.rdf+"type", n.foaf+"Organization"]], n.foaf+"name"]],
	"history":				[ITEM,			n.z+"history"],
	"reporter":				[CONTAINER,		n.dcterms+"title"],
	"court":				[CONTAINER,		n.bibo+"court"],
	"numberOfVolumes":		[CONTAINER_SERIES,		n.bibo+"numberOfVolumes"],
	"committee":			[ITEM,			[n.bibo+"organizer", [[n.rdf+"type", n.sc+"Committee_Organization"], [n.rdf+"type", n.foaf+"Organization"]], n.foaf+"name"]],
	"assignee":				[ITEM, 			n.z+"assignee"],			// TODO
	"priorityNumbers": 		[function(item) {							// TODO
		var priorityNumbers = item.priorityNumbers.split(/, ?| /g);
		var returnNumbers = [];
		for each(var number in priorityNumbers) {
			returnNumbers.push([ITEM, n.z+"priorityNumber", number, true]);
		}
		return returnNumbers;
	}, function(nodes) {
		var statements = Zotero.RDF.getStatementsMatching(nodes[ITEM], n.z+"priorityNumber", null);
		if(!statements) return false;
		var predicates = [];
		for each(var statement in statements) {
			predicates.push(statement[2]);
		}
		return predicates.join(", ");
	}],
	"references":			[ITEM,			n.z+"references"],
	"legalStatus":			[ITEM,			n.bibo+"status"],
	"codeNumber":			[CONTAINER,		n.bibo+"number"],
	"number":				[ITEM,			n.bibo+"number"],
	"artworkSize":			[ITEM,			n.dcterms+"extent"],
	"libraryCatalog":		[USERITEM,		n.z+"repository"],
	"archive":				[ITEM,			n.z+"repository"],
	"scale":				[ITEM,			n.z+"scale"],
	"meetingName":			[ITEM,			[n.bibo+"presentedAt", [[n.rdf+"type", n.bibo+"Conference"]], n.dcterms+"title"]],
	"runningTime":			[ITEM,			n.po+"duration"],
	"version":				[ITEM,			n.doap+"revision"],
	"system":				[ITEM, 			n.doap+"os"],
	"conferenceName":		[ITEM,			[n.bibo+"presentedAt", [[n.rdf+"type", n.bibo+"Conference"]], n.dcterms+"title"]],
	"language":				[ITEM,			n.dcterms+"language"],
	"programmingLanguage":	[ITEM,			n.doap+"programming-language"],
	"abstractNote":			[ITEM,			n.dcterms+"abstract"],
	"type":					[ITEM,			n.dcterms+"type"],
	"medium":				[ITEM,			n.dcterms+"medium"],
	"title":				[ITEM,			n.dcterms+"title"],
	"shortTitle":			[ITEM,			n.bibo+"shortTitle"],
	"numPages":				[ITEM,			n.bibo+"numPages"],
	"applicationNumber":	[ITEM,			n.z+"applicationNumber"],
	"issuingAuthority":		[ITEM,			[n.bibo+"issuer", [[n.rdf+"type", n.foaf+"Organization"]], n.foaf+"name"]],
	"filingDate":			[ITEM,			n.dcterms+"dateSubmitted"]
};

var AUTHOR_LIST = 1;
var EDITOR_LIST = 2;
var CONTRIBUTOR_LIST = 3;
var CREATOR_LISTS = {
	1:n.bibo+"authorList",
	2:n.bibo+"editorList",
	3:n.bibo+"contributorList"
};

var CREATORS = {
	"author":			[ITEM,			AUTHOR_LIST,		n.dcterms+"creator"],
	"attorneyAgent":	[ITEM,			CONTRIBUTOR_LIST,	n.z+"attorneyAgent"],
	"bookAuthor":		[CONTAINER,		AUTHOR_LIST,		n.dcterms+"creator"],
	"castMember":		[ITEM,			CONTRIBUTOR_LIST,	n.rel+"ACT"],
	"commenter":		[ITEM,			CONTRIBUTOR_LIST,	[n.sioct+"has_reply", [[n.rdf+"type", n.sioct+"Comment"]], n.dcterms+"creator"]],
	"composer":			[ITEM,			CONTRIBUTOR_LIST,	n.rel+"CMP"],
	"contributor":		[ITEM,			CONTRIBUTOR_LIST,	n.dcterms+"contributor"],
	"cosponsor":		[ITEM,			CONTRIBUTOR_LIST,	n.rel+"SPN"],
	"counsel":			[ITEM,			CONTRIBUTOR_LIST,	n.z+"counsel"],
	"director":			[ITEM,			CONTRIBUTOR_LIST,	n.bibo+"director"],
	"editor":			[SUBCONTAINER,	EDITOR_LIST,		n.bibo+"editor"],
	"guest":			[ITEM,			CONTRIBUTOR_LIST,	n.po+"participant"],
	"interviewer":		[ITEM,			CONTRIBUTOR_LIST,	n.bibo+"interviewer"],
	"interviewee":		[ITEM,			CONTRIBUTOR_LIST,	n.bibo+"interviewee"],
	"performer":		[ITEM,			CONTRIBUTOR_LIST,	n.bibo+"performer"],
	"producer":			[ITEM,			CONTRIBUTOR_LIST,	n.bibo+"producer"],
	"recipient":		[ITEM,			CONTRIBUTOR_LIST,	n.bibo+"recipient"],
	"reviewedAuthor":	[ITEM,			CONTRIBUTOR_LIST,	[n.bibo+"reviewOf", [], n.dcterms+"creator"]],
	"scriptwriter":		[ITEM,			CONTRIBUTOR_LIST,	n.rel+"AUS"],
	"seriesEditor":		[CONTAINER_SERIES,		EDITOR_LIST,		n.bibo+"editor"],
	"translator":		[SUBCONTAINER,	CONTRIBUTOR_LIST,	n.bibo+"translator"],
	"wordsBy":			[ITEM,			CONTRIBUTOR_LIST,	n.rel+"LYR"]
};

var SAME_ITEM_RELATIONS = [n.dcterms+"isPartOf", n.dcterms+"isVersionOf", n.bibo+"affirmedBy",
						   n.bibo+"presentedAt", n.bibo+"presents", n.bibo+"reproducedIn",
						   n.bibo+"reviewOf", n.bibo+"translationOf", n.bibo+"transcriptOf"];

/** COMMON FUNCTIONS **/

var BIBO_NS_LENGTH = n.bibo.length;
var RDF_TYPE = n.rdf+"type";

function getBlankNode(attachToNode, itemPredicate, blankNodePairs, create) {
	// check if a node with the same relation and properties already exists
	var blankNode = null;
	// look for blank node
	var statements1 = Zotero.RDF.getStatementsMatching(attachToNode, itemPredicate, undefined);
	for each(var statement1 in statements1) {
		// look for appropriate statements on the blank node
		var testNode = statement1[2];
		var statements2 = true;
		for each(var pair in blankNodePairs) {
			statements2 = Zotero.RDF.getStatementsMatching(testNode, pair[0], pair[1], false, true);
			if(!statements2) break;
		}
		if(statements2) {
			// if statements are good, then this is our node
			blankNode = testNode;
			break;
		}
	}
	
	// if no suitable node exists, generate a new one and add blank node statements
	if(!blankNode && create) {
		blankNode = Zotero.RDF.newResource();
		Zotero.RDF.addStatement(attachToNode, itemPredicate, blankNode, false);
		for each(var pair in blankNodePairs) {
			Zotero.RDF.addStatement(blankNode, pair[0], pair[1], false);
		}
	}
	
	return blankNode;
}

/**
 * A class representing a Zotero-to-BIBO type mapping
 * @property zoteroType {String} The corresponding Zotero type name
 */
Type = function(type, typeDefinition) {
	this.zoteroType = type;
	this[ITEM] = {"pairs":typeDefinition[0]};
	this[SUBCONTAINER] = typeDefinition[1] ? {"alwaysAdd":typeDefinition[1][0],
		"predicate":typeDefinition[1][1],
		"pairs":typeDefinition[1][2]} : null;
	this[CONTAINER] = typeDefinition[2] ? {"alwaysAdd":typeDefinition[2][0],
		"predicate":typeDefinition[2][1],
		"pairs":typeDefinition[2][2]} : null;
}

/**
 * Score a node to determine how well it matches our type definition
 * @returns {[Integer, Object]} The score, and an object containing ITEM, SUBCONTAINER, and 
 *                              CONTAINER nodes
 */
Type.prototype.getMatchScore = function(node) {
	var nodes = {2:node};
	
	// check item (+2 for each match, -1 for each nonmatch)
	var score = -this[ITEM].pairs.length;
	for each(var pair in this[ITEM].pairs) {
		if(Zotero.RDF.getStatementsMatching(node, pair[0], pair[1])) score += 3;
	}
	
	// check subcontainer
	[score, nodes[SUBCONTAINER]] = this._scoreNodeRelationship(node, this[SUBCONTAINER], score);
	// check container
	[score, nodes[CONTAINER]] = this._scoreNodeRelationship(
		(nodes[SUBCONTAINER] ? nodes[SUBCONTAINER] : nodes[ITEM]), this[CONTAINER], score);
	
	if(!nodes[CONTAINER]) nodes[CONTAINER] = nodes[ITEM];
	if(!nodes[SUBCONTAINER]) nodes[SUBCONTAINER] = nodes[CONTAINER];
	
	return [score, nodes];
}

/**
 * Score a CONTAINER/SUBCONTAINTER node
 * @returns {[Integer, Object]} The score, and the node (if it existed)
 */
Type.prototype._scoreNodeRelationship = function(node, definition, score) {
	var subNode = null;
	if(definition) {
		statements = Zotero.RDF.getStatementsMatching(node, definition.predicate, null);
		if(statements) {
			var bestScore = -9999;
			for each(var statement in statements) {
				// +2 for each match, -1 for each nonmatch
				var testScore = -definition.pairs.length;
				for each(var pair in definition.pairs) {
					if(Zotero.RDF.getStatementsMatching(statement[2], pair[0], pair[1])) score += 3;
				}
				
				if(testScore > bestScore) {
					subNode = statement[2];
					bestScore = testScore;
				}
			}
			score += bestScore;
		} else if(definition.alwaysAdd) {
			score -= definition.pairs.length;
		}
	}
	return [score, subNode];
}

/**
 * Get USERITEM and SERIES nodes for this type
 */
Type.prototype.getItemSeriesNodes = function(nodes) {
	const seriesDefinition = {"alwaysAdd":true, "predicate":n.dcterms+"isPartOf", "pairs":[[n.rdf+"type", n.bibo+"Series"]]};
	
	// get user item node
	var stmt = Zotero.RDF.getStatementsMatching(null, n.res+"resource", nodes[ITEM]);
	nodes[USERITEM] = stmt ? stmt[0][0] : nodes[ITEM];
	
	// get ITEM_SERIES node
	var score, subNode;
	[score, subNode] = this._scoreNodeRelationship(nodes[ITEM], seriesDefinition, 0);
	//Zotero.debug("got itemSeries with score "+score);
	if(score >= 1) nodes[ITEM_SERIES] = subNode;
	
	// get SUBCONTAINER_SERIES node
	[score, subNode] = this._scoreNodeRelationship(nodes[SUBCONTAINER], seriesDefinition, 0);
	//Zotero.debug("got subcontainerSeries with score "+score);
	if(score >= 1) nodes[CONTAINER_SERIES] = subNode;
	
	// get CONTAINER_SERIES node
	[score, subNode] = this._scoreNodeRelationship(nodes[CONTAINER], seriesDefinition, 0);
	//Zotero.debug("got containerSeries with score "+score);
	if(score >= 1) nodes[CONTAINER_SERIES] = subNode;
}

/**
 * Add triples to relate nodes. Called after all properties have been added, so we know which nodes
 * need to be related.
 */
Type.prototype.addNodeRelations = function(nodes) {
	// add node relations
	for each(var i in [ITEM_SERIES, SUBCONTAINER_SERIES, CONTAINER_SERIES]) {
		// don't add duplicate nodes
		if(!this[i-3]) continue;
		// don't add nodes with no arcs
		if(!Zotero.RDF.getArcsOut(nodes[i])) continue;
		Zotero.RDF.addStatement(nodes[i], RDF_TYPE, n.bibo+"Series", false);
		Zotero.RDF.addStatement(nodes[i-3], n.dcterms+"isPartOf", nodes[i], false);
	}
	
	for each(var i in [ITEM, SUBCONTAINER, CONTAINER]) {
		if(nodes[i]) {
			// find predicate
			if(i == ITEM) {
				var j = 1;
				var predicate = n.res+"resource";
			} else if(i == SUBCONTAINER || i == CONTAINER) {
				// don't add duplicate nodes
				if(!this[i]) continue;
				// don't add nodes with no arcs
				if(!this[i][0] && !Zotero.RDF.getArcsOut(nodes[i])) {
					nodes[i] = nodes[i-1];
					continue;
				}
				
				var predicate = this[i].predicate;
			}
			
			// add type
			for each(var pair in this[i].pairs) {
				Zotero.RDF.addStatement(nodes[i], pair[0], pair[1], false)
			}
			
			// add relation to parent
			for(var j = i-1; j>1; j--) {
				if(nodes[j] != nodes[i]) {
					Zotero.RDF.addStatement(nodes[j], predicate, nodes[i], false);
					break;
				}
			}
		}
	}
}

/**
 * Create USERITEM/ITEM/CONTAINER/SUBCONTAINER nodes for this type
 * @returns {Object} The created nodes
 */
Type.prototype.createNodes = function(item) {
	var nodes = {};
	nodes[USERITEM] = (item.uri ? item.uri : "#item_"+item.itemID);
	
	// come up with an item node URI
	nodes[ITEM] = null;
	// try the URL as URI
	if(item.url) {
		nodes[ITEM] = encodeURI(item.url);
		if(usedURIs[nodes[ITEM]]) nodes[ITEM] = null;
	}
	// try the DOI as URI
	if(!nodes[ITEM] && item.DOI) {
		var doi = item.DOI;
		if(doi.substr(0, 4) == "doi:") {
			doi = doi.substr(4);
		} else if(doi.substr(0, 8) == "urn:doi:") {
			doi = doi.substr(8);
		} else if(doi.substr(0, 9) == "info:doi/") {
			doi = doi.substr(9);
		} else if(doi.substr(0, 18) == "http://dx.doi.org/") {
			doi = doi.substr(18);
		}
		nodes[ITEM] = "info:doi/"+encodeURI(doi);
		if(usedURIs[nodes[ITEM]]) nodes[ITEM] = null;
	}
	// try the ISBN as URI
	if(!nodes[ITEM] && item.ISBN) {
		var isbn = item.ISBN.split(/, ?| /g)[0];
		nodes[ITEM] = "urn:isbn:"+encodeURI(isbn);
		if(usedURIs[nodes[ITEM]]) nodes[ITEM] = null;
	}
	// no suitable item URI; fall back to a blank node
	if(!nodes[ITEM]) nodes[ITEM] = Zotero.RDF.newResource();
	usedURIs[Zotero.RDF.getResourceURI(nodes[ITEM])] = true;
	
	// attach item node to user item node
	Zotero.RDF.addStatement(nodes[USERITEM], RDF_TYPE, n.z+"UserItem", false);
	Zotero.RDF.addStatement(nodes[USERITEM], n.res+"resource", nodes[ITEM], false);
	
	// container node
	nodes[CONTAINER] = (this[CONTAINER] ? Zotero.RDF.newResource() : nodes[ITEM]);
	
	// subcontainer node
	nodes[SUBCONTAINER] = (this[SUBCONTAINER] ? Zotero.RDF.newResource() : nodes[CONTAINER]);
	
	// series nodes
	nodes[ITEM_SERIES] = Zotero.RDF.newResource();
	nodes[CONTAINER_SERIES] = (this[CONTAINER] ? Zotero.RDF.newResource() : nodes[ITEM_SERIES]);
	nodes[SUBCONTAINER_SERIES] = (this[SUBCONTAINER] ? Zotero.RDF.newResource() : nodes[CONTAINER_SERIES]);
	
	return nodes;
}

/**
 * A class representing a BIBO-to-Zotero literal property mapping
 */
LiteralProperty = function(field) {
	this.field = field;
	this.mapping = FIELDS[field];
	if(!this.mapping) {
		//Zotero.debug("WARNING: unrecognized field "+field+" in Bibliontology RDF; mapping to Zotero namespace");
		this.mapping = [ITEM, n.z+field];
	}
}

/**
 * Maps property from a set of RDF nodes to an item
 */
LiteralProperty.prototype.mapToItem = function(newItem, nodes) {
	if(typeof this.mapping[0] == "function") {		// function case: triples returned
		// check function case
		var content = this.mapping[1](nodes);
		if(!content) return false;
		newItem[this.field] = content;
	} else {
		var node = nodes[this.mapping[0]];
		if(!node) return false;
		var statements = getStatementsByDefinition(this.mapping[1], node);
		if(!statements) return false;
		
		var content = [];
		for each(var stmt in statements) {
			content.push(stmt[2].toString());
		}
		newItem[this.field] = content.join(",");
	}
	return true;
}

/**
 * Maps property from an item to a set of RDF nodes
 */
LiteralProperty.prototype.mapFromItem = function(item, nodes) {
	if(typeof this.mapping[0] == "function") {				// function case: triples returned
		// check function case
		for each(var triple in this.mapping[0](item)) {
			Zotero.RDF.addStatement(nodes[triple[0]], triple[1], triple[2], triple[3])
		}
	} else if(typeof this.mapping[1] == "string") {		// string case: simple predicate
		Zotero.RDF.addStatement(nodes[this.mapping[0]],
			this.mapping[1], item.uniqueFields[this.field], true);
	} else {										// array case: complex predicate
		var blankNode = getBlankNode(nodes[this.mapping[0]],
			this.mapping[1][0], this.mapping[1][1], true);
		Zotero.RDF.addStatement(blankNode, this.mapping[1][2], item.uniqueFields[this.field], true);
	}
}

/**
 * A class representing a BIBO-to-Zotero creator mapping
 */
CreatorProperty = function(field) {
	this.field = field;
	this.mapping = CREATORS[field];
}

/**
 * Maps creator from an foaf:Agent
 */
CreatorProperty.prototype.mapToCreator = function(creatorNode, zoteroType) {
	//Zotero.debug("mapping "+Zotero.RDF.getResourceURI(creatorNode)+" to a creator");
	var lastNameStmt = Zotero.RDF.getStatementsMatching(creatorNode, n.foaf+"surname", null);
	if(lastNameStmt) {		// look for a person with a last name
		creator = {lastName:lastNameStmt[0][2].toString()};
		var firstNameStmt = Zotero.RDF.getStatementsMatching(creatorNode, n.foaf+"givenname", null);
		if(firstNameStmt) creator.firstName = firstNameStmt[0][2].toString();
	} else {
		var nameStmt = Zotero.RDF.getStatementsMatching(creatorNode, n.foaf+"name", null);
		if(nameStmt) {		// an organization
			creator = {lastName:nameStmt[0][2].toString(), fieldMode:1};
		} else {			// an unnamed entity; ignore it
			//Zotero.debug("Dropping unnamed creator "+creatorNode.toString());
			return false;
		}
	}
	
	// birthYear and shortName
	var birthStmt = Zotero.RDF.getStatementsMatching(creatorNode, n.foaf+"birthday", null, true);
	if(birthStmt) creator.birthYear = birthStmt[2].toString();
	var nickStmt = Zotero.RDF.getStatementsMatching(creatorNode, n.foaf+"nick", null, true);
	if(nickStmt) creator.shortName = nickStmt[2].toString();
	
	if(this.field == "author") {
		// could be another primary creator
		var creatorsForType = Zotero.Utilities.getCreatorsForType(zoteroType);
		if(creatorsForType.indexOf("author") == -1) {
			creator.creatorType = creatorsForType[0];
		}
	} else {
		creator.creatorType = this.field;
	}
	return creator;
}

/**
 * Maps creators from a top-level (ITEM/SUBCONTAINER/CONTAINER/SERIES) node to a list
 */
CreatorProperty.prototype.mapToCreators = function(node, zoteroType) {
	var creators = [];
	var creatorNodes = [];
	var statements = getStatementsByDefinition(this.mapping[2], node);
	if(statements) {
		for each(var stmt in statements) {
			var creator = this.mapToCreator(stmt[2], zoteroType);
			if(creator) {
				creators.push(creator);
				creatorNodes.push(stmt[2]);
			}
		}
	}
	return [creators, creatorNodes];
}

/**
 * Maps property from a Zotero creator array to a set of RDF nodes
 */
CreatorProperty.prototype.mapFromCreator = function(item, creator, nodes) {
	var creatorsForType = Zotero.Utilities.getCreatorsForType(item.itemType);
	var isPrimary = creatorsForType[0] == this.field;
	if(this.mapping) {
		var mapping = this.mapping;
	} else {
		if(isPrimary && creatorsForType.indexOf("author") == -1) {
			// treat other primary creators as dcterms:creators
			var mapping = CREATORS["author"];
		} else {
			//Zotero.debug("WARNING: unrecognized creator type "+this.field+" in Bibliontology RDF; mapping to Zotero namespace");
			var mapping = [ITEM, AUTHOR_LIST, n.z+this.field];
		}
	}
	
	var creatorNode = Zotero.RDF.newResource();
	if(creator.fieldMode == 1) {
		Zotero.RDF.addStatement(creatorNode, RDF_TYPE, n.foaf+"Organization");
		if(creator.lastName) Zotero.RDF.addStatement(creatorNode, n.foaf+"name", creator.lastName, true);
	} else {
		Zotero.RDF.addStatement(creatorNode, RDF_TYPE, n.foaf+"Person");
		if(creator.firstName) Zotero.RDF.addStatement(creatorNode, n.foaf+"givenname", creator.firstName, true);
		if(creator.lastName) Zotero.RDF.addStatement(creatorNode, n.foaf+"surname", creator.lastName, true);
	}
	if(creator.birthYear) Zotero.RDF.addStatement(creatorNode, n.foaf+"birthday", creator.birthYear, true);
	if(creator.shortName) Zotero.RDF.addStatement(creatorNode, n.foaf+"nick", creator.shortName, true);
	
	// attach creator node
	var attachTo = nodes[mapping[0]];
	if(typeof mapping[2] == "string") {
		var relation = mapping[2];
	} else {
		var relation = mapping[2][2];
		var attachTo = getBlankNode(attachTo, mapping[2][0], mapping[2][1], true);
	}
	Zotero.RDF.addStatement(attachTo, relation, creatorNode, false);
	
	// get appropriate creator list
	var list = mapping[1];
	if(list == CONTRIBUTOR_LIST && isPrimary) {
		// always attach primary to author list instead of contributor list
		list = AUTHOR_LIST;
	}
	
	// add to creator list
	var creatorList = Zotero.RDF.getStatementsMatching(nodes[mapping[0]], CREATOR_LISTS[list], null);
	if(creatorList) {
		var creatorList = creatorList[0][2];
	} else {
		var creatorList = Zotero.RDF.newResource();
		Zotero.RDF.newContainer("seq", creatorList);
		Zotero.RDF.addStatement(nodes[mapping[0]], CREATOR_LISTS[list], creatorList, false);
	}
	Zotero.RDF.addContainerElement(creatorList, creatorNode, false);
}

/** IMPORT FUNCTIONS **/

/**
 * Gets statements matching a statement definition, if it exists
 */
function getStatementsByDefinition(definition, node) {
	var statements = null;
	if(typeof definition == "string") {		// string case: simple predicate
		statements = Zotero.RDF.getStatementsMatching(node, definition, null);
	} else {								// array case: complex 
		var blankNode = getBlankNode(node, definition[0], definition[1], false);
		if(blankNode) {
			statements = Zotero.RDF.getStatementsMatching(blankNode, definition[2], null);
		}
	}
	return statements;
}

function detectImport() {
	// look for a bibo item type
	var rdfTypes = Zotero.RDF.getStatementsMatching(null, RDF_TYPE, null);
	if(rdfTypes) {
		for each(var rdfType in rdfTypes) {
			if(rdfType[2].uri && rdfType[2].uri.substr(0, BIBO_NS_LENGTH) == n.bibo) return true;
		}
	}
	return false;
}

function doImport() {
	// collapse list of BIBO-only types
	var collapsedTypes = {};
	for(var unprefixedBiboType in BIBO_TYPES) {
		var biboType = n.bibo+unprefixedBiboType;
		var type = new Type(BIBO_TYPES[unprefixedBiboType], [[[RDF_TYPE, n.bibo+biboType]], null, null]);
		if(!collapsedTypes[biboType]) {
			collapsedTypes[biboType] = [type];
		} else {
			collapsedTypes[biboType].push(type);
		}
	}
	
	// collapse Zotero-to-BIBO type mappings
	for(var zoteroType in TYPES) {
		var type = new Type(zoteroType, TYPES[zoteroType]);
		for each(var pair in TYPES[zoteroType][0]) {
			if(!collapsedTypes[pair[1]]) {
				collapsedTypes[pair[1]] = [type];
			} else {
				collapsedTypes[pair[1]].push(type);
			}
		}
	}
	
	// collapse list of field mappings
	var collapsedProperties = {1:{}, 2:{}, 3:{}, 4:{}, 5:{}, 6:{}, 7:{}};
	var functionProperties = {};
	for(var zoteroField in FIELDS) {
		if(typeof FIELDS[zoteroField][0] == "function") {
			functionProperties[zoteroField] = new LiteralProperty(zoteroField);
		} else {
			var domain = FIELDS[zoteroField][0];
			var predicate = FIELDS[zoteroField][1];
			if(typeof predicate == "object") predicate = predicate[0];
			var prop = new LiteralProperty(zoteroField);
		
			if(collapsedProperties[domain][predicate]) {
				collapsedProperties[domain][predicate].push(prop);
			} else {
				collapsedProperties[domain][predicate] = [prop];
			}
		}
	}
	
	// collapse list of creators
	for(var creatorType in CREATORS) {
		var domain = CREATORS[creatorType][0];
		var predicate = CREATORS[creatorType][2];
		if(typeof predicate == "object") predicate = predicate[0];
		var prop = new CreatorProperty(creatorType);
		
		if(collapsedProperties[domain][predicate]) {
			collapsedProperties[domain][predicate].unshift(prop);
		} else {
			collapsedProperties[domain][predicate] = [prop];
		}
	}
	
	// Go through all type arcs to find items
	var itemNode, predicateNode, objectNode;
	var rdfTypes = Zotero.RDF.getStatementsMatching(null, RDF_TYPE, null);
	var itemNodes = {};
	for each(var rdfType in rdfTypes) {
		[itemNode, predicateNode, objectNode] = rdfType;
		if(!objectNode.uri || !collapsedTypes[objectNode.uri]) continue;
		itemNodes[Zotero.RDF.getResourceURI(itemNode)] = itemNode;
	}
	
	// Look through found items to see if their rdf:type matches a Zotero item type URI, and if so,
	// subject to further processing
	for each(var itemNode in itemNodes) {
		// check whether the relationship to another item precludes us from extracting this as
		// top-level
		var skip = false;
		for each(var arc in Zotero.RDF.getArcsIn(itemNode)) {
			if(SAME_ITEM_RELATIONS.indexOf(arc) !== -1) {
				skip = true;
				break;
			}
		}
		if(skip) continue;
		
		var itemRDFTypes = Zotero.RDF.getStatementsMatching(itemNode, RDF_TYPE, null);
		
		// score types by the number of triples they share with our types
		var bestTypeScore = -9999;
		var bestType, score, nodes, bestNodes;
		for each(var rdfType in itemRDFTypes) {
			if(!rdfType[2].uri) continue;
			
			for each(var type in collapsedTypes[rdfType[2].uri]) {
				[score, nodes] = type.getMatchScore(itemNode);
				//Zotero.debug("Type "+type.zoteroType+" has score "+score);
				
				// check if this is the best we can do
				if(score > bestTypeScore) {
					bestTypeScore = score;
					bestType = type;
					bestNodes = nodes;
				}
			}
		}
		
		// skip if this doesn't fit any type very well
		if(bestTypeScore < 1) {
			//Zotero.debug("No good type mapping; best type was "+bestType.zoteroType+" with score "+bestTypeScore);
			continue;
		}
		
		//Zotero.debug("Got item of type "+bestType.zoteroType+" with score "+bestTypeScore);
		nodes = bestNodes;
		bestType.getItemSeriesNodes(nodes);
		
		// create item
		var zoteroType = bestType.zoteroType;
		var newItem = new Zotero.Item(zoteroType);
		
		// handle ordinary properties
		var allCreators = {}
		for(var i in nodes) {
			var propertiesHandled = {};
			var properties = Zotero.RDF.getArcsOut(nodes[i]);
			for each(var property in properties) {
				// only handle each property once
				if(propertiesHandled[property]) continue;
				propertiesHandled[property] = true;
				//Zotero.debug("handling "+property);
				
				var propertyMappings = collapsedProperties[i][property];
				if(propertyMappings) {
					for each(var propertyMapping in propertyMappings) {
						if(propertyMapping.mapToItem) {				// LiteralProperty
							propertyMapping.mapToItem(newItem, nodes);
						} else if(propertyMapping.mapToCreator) {	// CreatorProperty
							var creators, creatorNodes;
							[creators, creatorNodes] = propertyMapping.mapToCreators(nodes[i], zoteroType);
							if(creators.length) {
								for(var j in creators) {
									var creatorNodeURI = Zotero.RDF.getResourceURI(creatorNodes[j]);
									if(!allCreators[creatorNodeURI]) {
										allCreators[creatorNodeURI] = creators[j];
									}
								}
							}
						}
					}
				}
			}
		}
		
		// handle function properties
		for each(var functionProperty in functionProperties) {
			functionProperty.mapToItem(newItem, nodes);
		}
		
		// handle creators and tags
		var creatorLists = {};
		var creatorsAdded = {};
		for(var i in nodes) {
			// take DC subjects as tags
			var statements = Zotero.RDF.getStatementsMatching(nodes[i], n.dcterms+"subject", null);
			for each(var stmt in statements) {
				// if attached to the user item, it's a user tag; otherwise, an automatic tag
				newItem.tags.push({tag:stmt[2], type:(i == USERITEM ? 0 : 1)});
			}
			
			// also take ctags as tags
			var statements = Zotero.RDF.getStatementsMatching(nodes[i], n.ctag+"tagged", null);
			for each(var stmt in statements) {
				var tag = {type:0};
				
				// AutoTags are automatic tags
				var types = Zotero.RDF.getStatementsMatching(stmt[2], n.rdf+"type", null);
				for each(var type in types) {
					var uri = Zotero.RDF.getResourceURI(type[2]);
					if([n.ctag+"AutoTag", n.ctag+"AuthorTag"].indexOf(uri) != -1) tag.type = 1;
					break;
				}
				
				// labels are tag content
				var labels = Zotero.RDF.getStatementsMatching(stmt[2], n.ctag+"label", null);
				if(labels.length) {
					tag.tag = labels[0][2];
					newItem.tags.push(tag);
				}
			}
			
			for(var j in CREATOR_LISTS) {
				var statements = Zotero.RDF.getStatementsMatching(nodes[i], CREATOR_LISTS[j], null);
				for each(var stmt in statements) {
					var creatorListURI = Zotero.RDF.getResourceURI(stmt[2]);
					if(creatorLists[creatorListURI]) continue;
					creatorLists[creatorListURI] = true;
					var creatorNodes = Zotero.RDF.getContainerElements(stmt[2]);
					for each(var creatorNode in creatorNodes) {
						var creatorNodeURI = Zotero.RDF.getResourceURI(creatorNode);
						if(!creatorsAdded[creatorNodeURI]) {
							creatorsAdded[creatorNodeURI] = true;
							if(allCreators[creatorNodeURI]) {
								// just add to creators list
								newItem.creators.push(allCreators[creatorNodeURI]);
							} else {
								// creator not already processed, use default for this list type
								if(j == AUTHOR_LIST) {
									//Zotero.debug("WARNING: creator in authorList lacks relationship to item in Bibliontology RDF; treating as primary creator");
									var prop = new CreatorProperty("author");
								} else if(j == EDITOR_LIST) {
									//Zotero.debug("WARNING: creator in editorList lacks relationship to item in Bibliontology RDF; treating as editor");
									var prop = new CreatorProperty("editor");
								} else {
									//Zotero.debug("WARNING: creator in contributorList lacks relationship to item in Bibliontology RDF; treating as contributor");
									var prop = new CreatorProperty("contributor");
								}							
								var creator = prop.mapToCreator(creatorNode, zoteroType);
								if(creator) newItem.creators.push(creator);
							}
						}
					}
				}
			}
		}
		
		for(var creatorNodeURI in allCreators) {
			if(!creatorsAdded[creatorNodeURI]) {
				newItem.creators.push(allCreators[creatorNodeURI]);
			}
		}
		
		newItem.complete();
	}
}

/** EXPORT FUNCTIONS **/

var usedURIs = {};

function doExport() {
	// add namespaces
	for(var i in n) Zotero.RDF.addNamespace(i, n[i]);
	
	// compile references and create URIs
	var item;
	var items = {};
	while(item = Zotero.nextItem()) {
		items[item.itemID] = item;
	}
	var autoTags = {};
	var userTags = {};
	
	// now that we've collected our items, start building the RDF
	for each(var item in items) {
		// set type on item node
		var type = new Type(item.itemType, TYPES[item.itemType]);
		var nodes = type.createNodes(item);
		
		// add fields
		for(var field in item.uniqueFields) {
			if(item.uniqueFields[field] == "") continue;
			
			var property = new LiteralProperty(field);
			property.mapFromItem(item, nodes);
		}
		//Zotero.debug("fields added");
		
		// add creators
		var creatorLists = [];
		for each(var creator in item.creators) {
			// create creator
			var property = new CreatorProperty(creator.creatorType);
			property.mapFromCreator(item, creator, nodes);
		}
		//Zotero.debug("creators added");
		
		// add tags
		for each(var tag in item.tags) {
			var tagCollection = tag.type == 0 ? userTags : autoTags;
			
			if(tagCollection[tag.tag]) {
				var tagNode = tagCollection[tag.tag];
			} else {
				var tagNode = Zotero.RDF.newResource();
				Zotero.RDF.addStatement(tagNode, n.rdf+"type",
					(tag.type == 0 ? n.ctag+"UserTag" : n.ctag+"AutoTag"), false);
				Zotero.RDF.addStatement(tagNode, n.ctag+"label", tag.tag, true);
				tagCollection[tag.tag] = tagNode;
			}
			
			Zotero.RDF.addStatement(nodes[USERITEM], n.ctag+"tagged", tagNode, false);
		}
		
		type.addNodeRelations(nodes);
		//Zotero.debug("relations added");
	}
}