/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ this.EXPORTED_SYMBOLS = ['MimeType', 'MimeTypeNoun']; var Cc = Components.classes; var Ci = Components.interfaces; var Cr = Components.results; var Cu = Components.utils; Cu.import("resource:///modules/gloda/log4moz.js"); Cu.import("resource:///modules/StringBundle.js"); var LOG = Log4Moz.repository.getLogger("gloda.noun.mimetype"); Cu.import("resource:///modules/gloda/gloda.js"); var CategoryStringMap = {}; /** * Mime type abstraction that exists primarily so we can map mime types to * integer id's. * * Instances of this class should only be retrieved via |MimeTypeNoun|; no one * should ever create an instance directly. */ function MimeType(aID, aType, aSubType, aFullType, aCategory) { this._id = aID; this._type = aType; this._subType = aSubType; this._fullType = aFullType; this._category = aCategory; } MimeType.prototype = { /** * The integer id we have associated with the mime type. This is stable for * the lifetime of the database, which means that anything in the Gloda * database can use this without fear. Things not persisted in the database * should use the actual string mime type, retrieval via |fullType|. */ get id() { return this._id; }, /** * The first part of the MIME type; "text/plain" gets you "text". */ get type() { return this._type; }, set fullType(aFullType) { if (!this._fullType) { this._fullType = aFullType; [this._type, this._subType] = this._fullType.split("/"); this._category = MimeTypeNoun._getCategoryForMimeType(aFullType, this._type); } }, /** * If the |fullType| is "text/plain", subType is "plain". */ get subType() { return this._subType; }, /** * The full MIME type; "text/plain" returns "text/plain". */ get fullType() { return this._fullType; }, toString: function () { return this.fullType; }, /** * @return the category we believe this mime type belongs to. This category * name should never be shown directly to the user. Instead, use * |categoryLabel| to get the localized name for the category. The * category mapping comes from mimeTypesCategories.js. */ get category() { return this._category; }, /** * @return The localized label for the category from gloda.properties in the * "gloda.mimetype.category.CATEGORY.label" definition using the value * from |category|. */ get categoryLabel() { return CategoryStringMap[this._category]; } }; /** * Mime type noun provider. * * The set of MIME Types is sufficiently limited that we can keep them all in * memory. In theory it is also sufficiently limited that we could use the * parameter mechanism in the database. However, it is more efficient, for * both space and performance reasons, to store the specific mime type as a * value. For future-proofing reasons, we opt to use a database table to * persist the mapping rather than a hard-coded list. A preferences file or * other text file would arguably suffice, but for consistency reasons, the * database is not a bad thing. */ var MimeTypeNoun = { name: "mime-type", clazz: MimeType, // gloda supports clazz as well as class allowsArbitraryAttrs: false, _strings: new StringBundle("chrome://messenger/locale/gloda.properties"), // note! update test_noun_mimetype if you change our internals! _mimeTypes: {}, _mimeTypesByID: {}, TYPE_BLOCK_SIZE: 16384, _mimeTypeHighID: {}, _mimeTypeRangeDummyObjects: {}, _highID: 0, // we now use the exciting 'schema' mechanism of defineNoun to get our table // created for us, plus some helper methods that we simply don't use. schema: { name: 'mimeTypes', columns: [['id', 'INTEGER PRIMARY KEY', '_id'], ['mimeType', 'TEXT', 'fullType']], }, _init: function() { LOG.debug("loading MIME types"); this._loadCategoryMapping(); this._loadMimeTypes(); }, /** * A map from MIME type to category name. */ _mimeTypeToCategory: {}, /** * Load the contents of mimeTypeCategories.js and populate */ _loadCategoryMapping: function MimeTypeNoun__loadCategoryMapping() { let mimecatNS = {}; Cu.import("resource:///modules/gloda/mimeTypeCategories.js", mimecatNS); let mcm = mimecatNS.MimeCategoryMapping; let mimeTypeToCategory = this._mimeTypeToCategory; function procMapObj(aSubTree, aCategories) { for (let key in aSubTree) { let value = aSubTree[key]; // Add this category to our nested categories list. Use concat since // the list will be long-lived and each list needs to be distinct. let categories = aCategories.concat(); categories.push(key); if (categories.length == 1) { CategoryStringMap[key] = MimeTypeNoun._strings.get( "gloda.mimetype.category." + key + ".label"); } // Is it an array? If so, just process this depth if (Array.isArray(value)) { for (let mimeTypeStr of value) { mimeTypeToCategory[mimeTypeStr] = categories; } } // it's yet another sub-tree branch else { procMapObj(value, categories); } } } procMapObj(mimecatNS.MimeCategoryMapping, []); }, /** * Lookup the category associated with a MIME type given its full type and * type. (So, "foo/bar" and "foo" for "foo/bar".) */ _getCategoryForMimeType: function MimeTypeNoun__getCategoryForMimeType(aFullType, aType) { if (aFullType in this._mimeTypeToCategory) return this._mimeTypeToCategory[aFullType][0]; let wildType = aType + "/*"; if (wildType in this._mimeTypeToCategory) return this._mimeTypeToCategory[wildType][0]; return this._mimeTypeToCategory["*"][0]; }, /** * In order to allow the gloda query mechanism to avoid hitting the database, * we need to either define the noun type as cachable and have a super-large * cache or simply have a collection with every MIME type in it that stays * alive forever. * This is that collection. It is initialized by |_loadMimeTypes|. As new * MIME types are created, we add them to the collection. */ _universalCollection: null, /** * Kick off a query of all the mime types in our database, leaving * |_processMimeTypes| to actually do the legwork. */ _loadMimeTypes: function MimeTypeNoun__loadMimeTypes() { // get all the existing mime types! let query = Gloda.newQuery(this.id); let nullFunc = function() {}; this._universalCollection = query.getCollection({ onItemsAdded: nullFunc, onItemsModified: nullFunc, onItemsRemoved: nullFunc, onQueryCompleted: function (aCollection) { MimeTypeNoun._processMimeTypes(aCollection.items); } }, null); }, /** * For the benefit of our Category queryHelper, we need dummy ranged objects * that cover the numerical address space allocated to the category. We * can't use a real object for the upper-bound because the upper-bound is * constantly growing and there is the chance the query might get persisted, * which means these values need to be long-lived. Unfortunately, our * solution to this problem (dummy objects) complicates the second case, * should it ever occur. (Because the dummy objects cannot be persisted * on their own... but there are other issues that will come up that we will * just have to deal with then.) */ _createCategoryDummies: function (aId, aCategory) { let blockBottom = aId - (aId % this.TYPE_BLOCK_SIZE); let blockTop = blockBottom + this.TYPE_BLOCK_SIZE - 1; this._mimeTypeRangeDummyObjects[aCategory] = [ new MimeType(blockBottom, "!category-dummy!", aCategory, "!category-dummy!/" + aCategory, aCategory), new MimeType(blockTop, "!category-dummy!", aCategory, "!category-dummy!/" + aCategory, aCategory) ]; }, _processMimeTypes: function MimeTypeNoun__processMimeTypes(aMimeTypes) { for (let mimeType of aMimeTypes) { if (mimeType.id > this._highID) this._highID = mimeType.id; this._mimeTypes[mimeType] = mimeType; this._mimeTypesByID[mimeType.id] = mimeType; let typeBlock = mimeType.id - (mimeType.id % this.TYPE_BLOCK_SIZE); let blockHighID = (mimeType.category in this._mimeTypeHighID) ? this._mimeTypeHighID[mimeType.category] : undefined; // create the dummy range objects if (blockHighID === undefined) this._createCategoryDummies(mimeType.id, mimeType.category); if ((blockHighID === undefined) || mimeType.id > blockHighID) this._mimeTypeHighID[mimeType.category] = mimeType.id; } }, _addNewMimeType: function MimeTypeNoun__addNewMimeType(aMimeTypeName) { let [typeName, subTypeName] = aMimeTypeName.split("/"); let category = this._getCategoryForMimeType(aMimeTypeName, typeName); if (!(category in this._mimeTypeHighID)) { let nextID = this._highID - (this._highID % this.TYPE_BLOCK_SIZE) + this.TYPE_BLOCK_SIZE; this._mimeTypeHighID[category] = nextID; this._createCategoryDummies(nextID, category); } let nextID = ++this._mimeTypeHighID[category]; let mimeType = new MimeType(nextID, typeName, subTypeName, aMimeTypeName, category); if (mimeType.id > this._highID) this._highID = mimeType.id; this._mimeTypes[aMimeTypeName] = mimeType; this._mimeTypesByID[nextID] = mimeType; // As great as the gloda extension mechanisms are, we don't think it makes // a lot of sense to use them in this case. So we directly trigger object // insertion without any of the grokNounItem stuff. this.objInsert.call(this.datastore, mimeType); // Since we bypass grokNounItem and its fun, we need to explicitly add the // new MIME-type to _universalCollection ourselves. Don't try this at // home, kids. this._universalCollection._onItemsAdded([mimeType]); return mimeType; }, /** * Map a mime type to a |MimeType| instance, creating it if necessary. * * @param aMimeTypeName The mime type. It may optionally include parameters * (which will be ignored). A mime type is of the form "type/subtype". * A type with parameters would look like 'type/subtype; param="value"'. */ getMimeType: function MimeTypeNoun_getMimeType(aMimeTypeName) { // first, lose any parameters let semiIndex = aMimeTypeName.indexOf(";"); if (semiIndex >= 0) aMimeTypeName = aMimeTypeName.substring(0, semiIndex); aMimeTypeName = aMimeTypeName.trim().toLowerCase(); if (aMimeTypeName in this._mimeTypes) return this._mimeTypes[aMimeTypeName]; else return this._addNewMimeType(aMimeTypeName); }, /** * Query helpers contribute additional functions to the query object for the * attributes that use the noun type. For example, we define Category, so * for the "attachmentTypes" attribute, "attachmentTypesCategory" would be * exposed. */ queryHelpers: { /** * Query for MIME type categories based on one or more MIME type objects * passed in. We want the range to span the entire block allocated to the * category. * * @param aAttrDef The attribute that is using us. * @param aArguments The actual arguments object that */ Category: function(aAttrDef, aArguments) { let rangePairs = []; // If there are no arguments then we want to fall back to the 'in' // constraint which matches on any attachment. if (aArguments.length == 0) return this._inConstraintHelper(aAttrDef, []); for (let iArg = 0; iArg < aArguments.length; iArg++) { let arg = aArguments[iArg]; rangePairs.push(MimeTypeNoun._mimeTypeRangeDummyObjects[arg.category]); } return this._rangedConstraintHelper(aAttrDef, rangePairs); } }, comparator: function gloda_noun_mimeType_comparator(a, b) { if (a == null) { if (b == null) return 0; else return 1; } else if (b == null) { return -1; } return a.fullType.localeCompare(b.fullType); }, toParamAndValue: function gloda_noun_mimeType_toParamAndValue(aMimeType) { return [null, aMimeType.id]; }, toJSON: function gloda_noun_mimeType_toJSON(aMimeType) { return aMimeType.id; }, fromJSON: function gloda_noun_mimeType_fromJSON(aMimeTypeID) { return this._mimeTypesByID[aMimeTypeID]; }, }; Gloda.defineNoun(MimeTypeNoun, Gloda.NOUN_MIME_TYPE); try { MimeTypeNoun._init(); } catch (ex) { LOG.error("problem init-ing: " + ex.fileName + ":" + ex.lineNumber + ": " + ex); }